修改提交
512
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md
Normal file
@@ -0,0 +1,512 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Codely Bridge will be documented in this file.
|
||||
|
||||
## [1.0.23] - 2026-02-25
|
||||
|
||||
### Changed
|
||||
|
||||
- **UI**
|
||||
- Codely bridge 页面改版
|
||||
|
||||
## [1.0.22] - 2026-02-11
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ExecuteCSharpScript**
|
||||
- Added Unity.InputSystem assembly support for C# script execution
|
||||
- Scripts can now access Input System types and APIs when the package is installed
|
||||
|
||||
## [1.0.21] - 2026-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- **Batch Operations Refactoring**
|
||||
- Split generic `batch` action into two distinct operations: `create_batch` for write-only deterministic sequences and `edit_batch` for search-then-write edits
|
||||
- Added `HandleCreateBatch` method for write-only deterministic batch operations
|
||||
- Added `HandleEditBatch` method for search-then-write batch operations with captureAs support
|
||||
- Improved code clarity and prevented mixed read/write batch states
|
||||
- Maintained parity with TypeScript client schema
|
||||
- Added backward compatibility aliases for snake_case to camelCase parameters
|
||||
- Updated ValidActions list to include new batch operation types
|
||||
- Updated writeActions array to include new batch operations for state validation
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageAsset**
|
||||
- Enhanced batch operation handling with clearer separation of concerns
|
||||
- Improved parameter naming consistency with backward compatibility support
|
||||
|
||||
- **ManageGameObject**
|
||||
- Enhanced batch operation handling with clearer separation of concerns
|
||||
- Improved parameter naming consistency with backward compatibility support
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Updated `unity_asset_full_coverage.md` to reflect new batch operations
|
||||
- Updated `unity_gameobject_full_coverage.md` to reflect new batch operations
|
||||
- Updated `unity_workflow_full_coverage.md` with refined batch operation workflows
|
||||
- Updated `Tests/Coverage/README.md` documentation
|
||||
|
||||
|
||||
## [1.0.20] - 2026-02-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Port Management on macOS**
|
||||
- Disabled ReuseAddress socket option on macOS in PortManager.cs
|
||||
|
||||
|
||||
## [1.0.19] - 2026-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Port Management on macOS**
|
||||
- Disabled ReuseAddress socket option on macOS to prevent multiple Unity instances from listening on the same port
|
||||
- Ensures proper port exclusivity across Unity Editor instances
|
||||
|
||||
|
||||
## [1.0.18] - 2026-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- **TCP Connection Reliability**
|
||||
- Add LingerState to test listener to send RST on close (same as actual listener)
|
||||
- Increase immediate retry attempts from 3 to 5
|
||||
- Increase retry sleep time from 75ms to 150ms
|
||||
- Extend wait time on Windows from 100ms to 500ms to allow TCP port full release
|
||||
|
||||
|
||||
## [1.0.17] - 2026-02-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Revert IPV6 Loopback Support**
|
||||
|
||||
|
||||
## [1.0.16] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- **C# Script Execution**
|
||||
- New `ExecuteCSharpScript` tool for executing arbitrary C# code at runtime using Microsoft.CodeAnalysis.CSharp.Scripting (Roslyn compiler services)
|
||||
- Captures and returns Unity console logs during script execution
|
||||
- Supports custom using directives and assembly references
|
||||
- Enables dynamic C# code execution without requiring editor restart or recompilation
|
||||
- Added bundled Roslyn assemblies: `Codely.Microsoft.CodeAnalysis.dll`, `Codely.Microsoft.CodeAnalysis.CSharp.dll`, `Codely.Microsoft.CodeAnalysis.Scripting.dll`, `Codely.Microsoft.CodeAnalysis.CSharp.Scripting.dll`
|
||||
- Added supporting assemblies: `Codely.System.Collections.Immutable.dll`, `Codely.System.Reflection.Metadata.dll`, `Codely.System.Runtime.CompilerServices.Unsafe.dll`
|
||||
|
||||
### Fixed
|
||||
|
||||
- **UnityTcpBridge**
|
||||
- Reverted accidental handshake string change that broke package functionality (changed from incorrect 'WELCOME UNITY-TCP 1 FRAMING=1' back to correct 'WELCOME Codely-Bridge 1 FRAMING=1')
|
||||
- **Add IPV6 Loopback Support**
|
||||
|
||||
### Changed
|
||||
|
||||
- **Branding & Menu Structure**
|
||||
- Renamed "Unity TCP" to "Codely Bridge" across logging and port management
|
||||
- Simplified menu structure: removed redundant menu items and kept only "Window/Codely Bridge/Control Window"
|
||||
- Updated menu organization for better user experience
|
||||
|
||||
- **CI/CD**
|
||||
- Updated register_version job rules in CI pipeline
|
||||
|
||||
## [1.0.15] - 2026-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
- **ManageScreenshot**
|
||||
- Simplified screenshot API: removed redundant `capture_game_view` action; its behavior is now covered by the unified `capture` action
|
||||
- `capture` action now consistently uses GameView reflection to capture what the user sees in both edit and play modes
|
||||
- Removed redundant `FlipTextureVertically` call and related comments for clearer, more maintainable code
|
||||
|
||||
- **Package Publishing**
|
||||
- Updated `.npmignore` to include `Tests/` directory in the published npm package so consumers can run and extend the test suite
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Screenshot Capture**
|
||||
- Fixed vertical flip of textures captured from RenderTexture so screenshots match on-screen orientation
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Updated `unity_screenshot_full_coverage.md` and `Tests/Coverage/README.md` to reflect the simplified screenshot API and current test structure
|
||||
|
||||
## [1.0.14] - 2026-01-22
|
||||
|
||||
### Changed
|
||||
|
||||
- **Dependency Management**
|
||||
- Swapped to bundled `Codely.Newtonsoft.Json.dll` in `Plugins/` directory instead of external `Newtonsoft.Json` package
|
||||
- Removed external `Newtonsoft.Json` dependency from `package.json`
|
||||
- Updated all code references to use bundled Newtonsoft.Json assembly
|
||||
|
||||
- **Architecture**
|
||||
- Moved runtime implementation code from `Runtime/` to `Editor/` scope to align with Unity usage patterns
|
||||
|
||||
### Added
|
||||
|
||||
- **Package Publishing**
|
||||
- Added `.npmignore` file to exclude unwanted files from npm publication
|
||||
- Excludes CI/CD configuration, build artifacts, Git metadata, IDE files, and test directories
|
||||
- Ensures only essential code and documentation are published to npm registry
|
||||
|
||||
- **CI/CD Pipeline**
|
||||
- Added automated npm pack and TOS (Tencent Object Storage) upload steps to deployment pipelines
|
||||
- Added backup version (1.0.0) upload for both staging and production environments
|
||||
- Improved deployment reliability with fallback version availability
|
||||
|
||||
## [1.0.13] - 2026-01-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Test Assembly Dependencies**
|
||||
- Fixed missing `Newtonsoft.Json.dll` reference in `UnityTcp.Editor.Tests.asmdef`
|
||||
- Added `com.unity.ext.nunit` package dependency to `package.json` for proper NUnit framework support
|
||||
- Ensures test assembly can properly reference required dependencies for compilation
|
||||
|
||||
## [1.0.12] - 2026-01-19
|
||||
|
||||
### Added
|
||||
|
||||
- **ManageGameObject**
|
||||
- Added `list_children` action for listing GameObject children with configurable depth
|
||||
- Support for three result modes: `auto` (default), `inline`, and `file`
|
||||
- Automatic fallback to file output when hierarchy exceeds `maxInlineItems` threshold (default: 200)
|
||||
- Depth-limited traversal with `depth` parameter (default: 1 for direct children)
|
||||
- `includeInactive` parameter to control whether inactive GameObjects are included
|
||||
- Iterative tree building to avoid stack overflow on deep hierarchies
|
||||
- JSON streaming to file for large results to prevent memory issues
|
||||
- New helper methods: `CountDescendantsUpToDepth`, `BuildChildrenTreeIterative`, `WriteChildrenTreeIterative`
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageScene**
|
||||
- Improved large scene hierarchy handling (>500 GameObjects)
|
||||
- Returns shallow root-only tree with hints instead of error when scene is too large
|
||||
- Changed `CountGameObjectsRecursive` to use iterative traversal (stack-based) to avoid stack overflow on deep hierarchies
|
||||
- Better user guidance for drilling down into large hierarchies incrementally
|
||||
|
||||
- **Coverage Tools**
|
||||
- Added `CodelyUnityCoverageTools` class for E2E test utilities
|
||||
- New `codely.generate_large_hierarchy` custom tool for quickly generating test hierarchies
|
||||
- Configurable generation parameters: root name, child/grandchild prefixes, and counts
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Added `unity_large_hierarchy_e2e_coverage.md` with 152 lines of E2E test scenarios for large hierarchy handling
|
||||
- Updated `unity_gameobject_full_coverage.md` with `list_children` action coverage
|
||||
- Updated `unity_scene_full_coverage.md` with improved large scene handling coverage
|
||||
|
||||
## [1.0.11] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
||||
- **Tuanjie Editor Scene File Support**
|
||||
- Added support for `.scene` file extension used by Tuanjie Editor
|
||||
- Implemented extension-aware scene path handling in `ManageScene`
|
||||
- Support both `.unity` (Unity Editor) and `.scene` (Tuanjie Editor) extensions based on editor type
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **UnityStateDirtyHook**
|
||||
- Added `.scene` extension detection for scene file change tracking
|
||||
- Ensures proper state tracking for both Unity and Tuanjie editor scene files
|
||||
|
||||
- **Documentation**
|
||||
- Improved documentation for scene file extension handling
|
||||
|
||||
## [1.0.10] - 2026-01-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- **ManageGameObject**
|
||||
- Updated default behavior for `searchInactive` parameter from `false` to `true`
|
||||
- Ensures inactive GameObjects are included in search results unless explicitly specified otherwise
|
||||
|
||||
- **UnityEngineObjectConverter**
|
||||
- Added support for `{"find":"...", "method":"..."}` reference format in object deserialization
|
||||
- Fixed deserialization errors when encountering find instruction format used by MCP tools for dynamic GameObject lookups
|
||||
- Implemented delegate pattern to call `ManageGameObject.FindObjectByInstruction` from Runtime assembly without direct Editor assembly reference
|
||||
|
||||
## [1.0.9] - 2026-01-05
|
||||
|
||||
### Changed
|
||||
|
||||
- **Unity Project Metadata**
|
||||
- Updated Unity project metadata GUIDs across all .meta files
|
||||
- Refreshed GUIDs in Editor, Runtime, and Tests directories
|
||||
|
||||
## [1.0.8] - 2025-12-16
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **Compilation Tracking**
|
||||
- Improved compilation error/warning count tracking with nullable integers
|
||||
- Changed `CompilationHelper.GetCompilationErrors()` and `GetCompilationWarnings()` to return `int?` instead of `int`
|
||||
- Updated `GetCompilationSummary()` to only include known values in result (removes misleading default 0 values)
|
||||
- Enhanced compilation result handling in `ManageEditor` to properly process nullable values
|
||||
- Improved type handling for compilation result payloads (supports both dictionary and anonymous object formats)
|
||||
- Added clarifying comments explaining why returning 0 for unknown counts is problematic
|
||||
- Better distinction between "0 errors/warnings" (validated) vs "unknown count" (not yet validated)
|
||||
|
||||
## [1.0.7] - 2025-12-15
|
||||
|
||||
### Changed
|
||||
|
||||
- **Package Renaming**
|
||||
- Renamed package from `com.unity.codely` to `cn.tuanjie.codely.bridge`
|
||||
- Updated all internal references and documentation to reflect new package name
|
||||
|
||||
- **Branding Update**
|
||||
- Renamed all menu item paths and labels in the Unity Editor
|
||||
|
||||
## [1.0.6] - 2025-12-10
|
||||
|
||||
### Added
|
||||
|
||||
- **Compilation Pipeline**
|
||||
- New `pipeline_kind: "compile"` field for structured hints to downstream tools
|
||||
- `requires_console_validation: true` flag to guide compilation validation
|
||||
- Comprehensive integration test documentation for compilation pipeline policy
|
||||
- Enhanced play mode state synchronization with `playMode` field in responses
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **StateComposer**
|
||||
- Simplified state reporting to focus on essential information (compiling vs idle)
|
||||
- Minimized state complexity with clear documentation directing users to specific diagnostic tools
|
||||
|
||||
- **ManageAsset**
|
||||
- Improved `AssetExists` method with ghost asset detection
|
||||
- New `BuildAssetNotFoundResponse` for better error messaging about desync issues
|
||||
- Enhanced asset validation and error handling
|
||||
|
||||
- **Test Coverage**
|
||||
- Added `unity_compile_pipeline_integration.md` with 152 lines of comprehensive test scenarios
|
||||
- Updated `unity_editor_full_coverage.md` with compilation pipeline requirements
|
||||
|
||||
## [1.0.5] - 2025-12-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Test Coverage**
|
||||
- Added comprehensive test coverage for `ExecuteCustomTool` functionality
|
||||
- UI Toolkit tools test coverage documentation and validation
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **ManageUIToolkit**
|
||||
- Enhanced `link_uss_to_uxml` action with GUID support
|
||||
- Added `ResolveAssetPath` helper method for flexible path/GUID resolution
|
||||
- Improved parameter validation for mixed path/GUID usage
|
||||
|
||||
- **ManageShader**
|
||||
- Enhanced `ensure_material_shader_for_srp` action with `material_guid` parameter support
|
||||
- Improved parameter handling for material identification via path or GUID
|
||||
- Better error messages for missing material parameters
|
||||
|
||||
- **CodelyUnityValidationTools**
|
||||
- Added nested field path support in `validate_response`
|
||||
- Support for dot-notation field paths (e.g., "state.project.srp", "project.srp")
|
||||
- Automatic recursive field search when direct path fails
|
||||
- Enhanced field validation with path tracking and debugging
|
||||
|
||||
- **ManageBake**
|
||||
- Refactored NavMesh operations to use runtime reflection
|
||||
- Improved AI Navigation package detection and type resolution
|
||||
- Better compatibility with optional AI Navigation package installation
|
||||
- Enhanced error handling for missing package scenarios
|
||||
|
||||
## [1.0.4] - 2025-12-03
|
||||
|
||||
### Added
|
||||
|
||||
- **Validation Tools Framework**
|
||||
- New `CodelyUnityValidationTools`: 15+ validation helpers for automated testing
|
||||
- `codely.validate_play_mode`: Validate current editor PlayMode state
|
||||
- `codely.validate_active_tool`: Validate current active editor tool
|
||||
- `codely.validate_not_compiling`: Ensure editor is not compiling
|
||||
- `codely.validate_tag_and_layer_exist`: Verify Tag/Layer existence
|
||||
- `codely.validate_window_open`: Check editor window state
|
||||
- `codely.validate_console_contains`: Validate console messages with filter
|
||||
- `codely.validate_console_count`: Verify console message counts
|
||||
- `codely.validate_active_scene`: Validate active scene properties
|
||||
- `codely.validate_scene_dirty`: Check scene dirty state
|
||||
- `codely.validate_hierarchy_root_count`: Verify hierarchy root object count
|
||||
- `codely.validate_gameobject_exists`: Check GameObject existence
|
||||
- `codely.validate_response`: Generic response validation
|
||||
|
||||
- **Compilation Pipeline**
|
||||
- `CompilationHelper`: New helper class for compilation status checking and error tracking
|
||||
- `start_compilation_pipeline` action in ManageEditor for standardized compile workflow
|
||||
- Block compilation during play mode to prevent editor errors
|
||||
|
||||
- **Test Coverage Documentation**
|
||||
- Complete test coverage specs for all Unity tools
|
||||
- `unity_editor_full_coverage.md`: 24 actions coverage
|
||||
- `unity_console_full_coverage.md`: Console operations coverage
|
||||
- `unity_scene_full_coverage.md`: Scene management coverage
|
||||
- `unity_gameobject_full_coverage.md`: GameObject operations coverage
|
||||
- `unity_asset_full_coverage.md`: Asset management coverage
|
||||
- `unity_script_full_coverage.md`: Script management coverage
|
||||
- `unity_shader_full_coverage.md`: Shader operations coverage
|
||||
- `unity_package_full_coverage.md`: Package manager coverage
|
||||
- `unity_menu_full_coverage.md`: Menu execution coverage
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **State Management**
|
||||
- State delta tracking added to async operation responses
|
||||
- Client state revision validation for all write operations
|
||||
- Enhanced console state tracking with `since_token` filtering
|
||||
|
||||
- **ManageEditor**
|
||||
- Idempotent `ensure_tag` and `ensure_layer` operations
|
||||
- Extended with compilation pipeline integration
|
||||
|
||||
- **ManageAsset**
|
||||
- Enhanced robustness with better error handling
|
||||
|
||||
- **ManageGameObject**
|
||||
- Improved serialization with `GameObjectSerializer` enhancements
|
||||
|
||||
- **ReadConsole**
|
||||
- Enhanced filtering with `since_token` support for incremental reads
|
||||
|
||||
- **ExecuteCustomTool**
|
||||
- Improved tool registry with better parameter validation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved Unity version compatibility across various tools
|
||||
|
||||
## [1.0.3] - 2025-12-01
|
||||
|
||||
### Added
|
||||
|
||||
- **State Management System**
|
||||
- `AsyncOperationTracker`: Comprehensive async operation management with progress tracking and cancellation support
|
||||
- `StateComposer`: Full Unity state composition including scene, project, packages, and shaders
|
||||
- `UnityStateDirtyHook`: Automatic tracking of Unity Editor state changes (hierarchy, project, selection, console)
|
||||
- `WriteGuard`: Thread-safe write operation protection with main thread enforcement
|
||||
- New `get_current_state` endpoint for retrieving complete Unity state snapshots
|
||||
|
||||
- **New Unity Tools**
|
||||
- `ManageBake`: Light baking controls (start, cancel, clear, status queries)
|
||||
- `ManagePackage`: Package manager operations with version pinning support (package@version syntax)
|
||||
- `ManageUIToolkit`: UI Toolkit template instantiation with automatic USS/C# generation
|
||||
|
||||
- **Custom Tool Execution Framework**
|
||||
- `ExecuteCustomTool`: Reflection-based tool discovery and execution via `[CustomTool]` attribute
|
||||
- Automatic tool registry with parameter validation and error handling
|
||||
- Support for custom tools without modifying CommandRegistry
|
||||
|
||||
- **Enhanced Existing Tools**
|
||||
- `ManageEditor`: Extended with state-aware operations and full state retrieval
|
||||
- `ManageGameObject`: Added find, query, parent/child operations, and component management
|
||||
- `ManageAsset`: New asset import, export, and metadata operations
|
||||
- `ManageScene`: Enhanced with scene creation and multi-scene management
|
||||
- `ManageShader`: Expanded with shader compilation, variant queries, and global property management
|
||||
- `ReadConsole`: Added scope-based console clearing and entry filtering
|
||||
|
||||
### Enhanced
|
||||
|
||||
- **Response Helpers**: New state-aware methods (`SuccessWithDelta`, `SuccessWithState`, `Conflict`) for better change tracking
|
||||
- **CompilationHelper**: Improved compilation workflow handling with better async integration
|
||||
- **Test Coverage**: Added unit tests for `AsyncOperationTracker`, `StateComposer`, and `WriteGuard`
|
||||
|
||||
## [1.0.2] - 2025-11-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Unity Version Compatibility**: Added conditional compilation in `ManageGameObject.cs`
|
||||
- Uses `FindObjectsByType` with `FindObjectsInactive` enum for Unity 2022.2+
|
||||
- Falls back to `FindObjectsOfType` for Unity 2021.3 and earlier
|
||||
- Resolves CS0246 error: `FindObjectsInactive` type not found on Unity 2021
|
||||
- Maintains backward compatibility across Unity versions
|
||||
|
||||
## [1.0.1] - 2025-11-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛 **Fixed build compilation error**
|
||||
- Corrected assembly definition configuration for `UnityTcp.Editor.asmdef`
|
||||
- Changed from platform exclusion list to explicit Editor platform inclusion
|
||||
- Ensures Editor assembly only compiles in Unity Editor, not in game builds
|
||||
- Resolves compilation errors during game packaging for all platforms
|
||||
|
||||
## [1.0.0] - 2024-12-19
|
||||
|
||||
### Major Refactoring
|
||||
|
||||
- 🔄 **Complete removal of MCP (Model Context Protocol) logic**
|
||||
- Removed all MCP-specific components, tools, and protocol handling
|
||||
- Eliminated MCP server integration and HTTP server components
|
||||
- Removed MCP client models, configuration systems, and UI windows
|
||||
|
||||
### New TCP-Focused Architecture
|
||||
|
||||
- 🚀 **Pure TCP Socket Implementation**
|
||||
- New `UnityTcpBridge` class for TCP server management
|
||||
- Basic echo server implementation as starting point
|
||||
- Async/await patterns for non-blocking operations
|
||||
- Multi-client connection support with proper resource management
|
||||
|
||||
### Core TCP Features
|
||||
|
||||
- **Port Management**
|
||||
- Automatic port discovery and allocation
|
||||
- Project-specific port persistence
|
||||
- Smart port conflict resolution
|
||||
- Cross-platform compatibility
|
||||
|
||||
- **Connection Handling**
|
||||
- TCP listener with automatic client acceptance
|
||||
- Configurable socket options (keep-alive, timeouts)
|
||||
- Graceful connection cleanup on shutdown
|
||||
- Unity lifecycle integration (assembly reload, editor quit)
|
||||
|
||||
### Updated Components
|
||||
|
||||
- **Renamed Assemblies**: `UnityTcp.*` → `UnityTcp.*`
|
||||
- **Updated Namespaces**: All classes moved to `UnityTcp.Editor.*` namespace
|
||||
- **Simplified Helpers**: Kept only TCP-relevant utilities (PortManager, TcpLog)
|
||||
- **Package Rebranding**: Updated from "Unity MCP" to "Unity TCP Bridge"
|
||||
|
||||
### Removed Components
|
||||
|
||||
- All MCP protocol handling and message processing
|
||||
- MCP tool implementations (ManageScript, ManageAsset, etc.)
|
||||
- MCP UI windows and editor integrations
|
||||
- HTTP server and MCP server management
|
||||
- Telemetry and MCP-specific logging
|
||||
- Configuration builders and MCP client models
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Architecture**: Direct TCP socket server with customizable protocol handling
|
||||
- **Performance**: Lightweight implementation focused on TCP networking
|
||||
- **Compatibility**: Unity 2021.3+ with Newtonsoft.Json dependency
|
||||
- **Protocol**: Basic TCP with welcome handshake (easily customizable)
|
||||
|
||||
### Migration Guide
|
||||
|
||||
This is a breaking change that removes all MCP functionality:
|
||||
|
||||
1. **Previous MCP Users**: This package no longer provides MCP integration
|
||||
2. **TCP Socket Users**: Replace any `UnityTcpBridge` references with `UnityTcpBridge`
|
||||
3. **Custom Protocols**: Implement your protocol logic in `HandleClientAsync` method
|
||||
4. **Port Management**: Use `PortManager` for dynamic port allocation needs
|
||||
|
||||
### Development Notes
|
||||
|
||||
- Codebase reduced by ~80% by removing MCP complexity
|
||||
- Focus shifted to providing a clean TCP socket foundation
|
||||
- Easy to extend for custom networking protocols
|
||||
- Maintains Unity Editor integration for automatic lifecycle management
|
||||
|
||||
## Previous Versions
|
||||
|
||||
Previous versions (1.x.x) included MCP (Model Context Protocol) integration which has been completely removed in this version.
|
||||
7
Packages/cn.tuanjie.codely.bridge/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cb996b11fa557143a68a6fdb4f0cb76
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff37f8f7a26ddcf4a8ec576dd133285c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/cn.tuanjie.codely.bridge/Editor/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("UnityTcpTests.EditMode")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7083f648bddd50d41afc42cc3deb2577
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor/Helpers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 626ae8824a7df62489152ef051a0e2a9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages long-running asynchronous operation states (compilation, UPM, baking, etc.)
|
||||
/// Provides operation ID generation, status tracking, and timeout management.
|
||||
/// </summary>
|
||||
public static class AsyncOperationTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Job status enum matching MCP protocol.
|
||||
/// </summary>
|
||||
public enum JobStatus
|
||||
{
|
||||
Pending,
|
||||
Complete,
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Job type enum for categorizing operations.
|
||||
/// </summary>
|
||||
public enum JobType
|
||||
{
|
||||
Compilation,
|
||||
UpmPackage,
|
||||
NavMeshBake,
|
||||
LightingBake,
|
||||
Custom
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tracked job/operation.
|
||||
/// </summary>
|
||||
public class Job
|
||||
{
|
||||
public string OpId { get; set; }
|
||||
public JobType Type { get; set; }
|
||||
public JobStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
public string Message { get; set; }
|
||||
public object Data { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public float Progress { get; set; } // 0.0 to 1.0
|
||||
}
|
||||
|
||||
// Job storage
|
||||
private static readonly Dictionary<string, Job> _jobs = new Dictionary<string, Job>();
|
||||
private static readonly object _jobsLock = new object();
|
||||
|
||||
// Default timeout in seconds
|
||||
private const int DefaultTimeoutSeconds = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new job with a unique op_id and registers it.
|
||||
/// </summary>
|
||||
public static Job CreateJob(JobType type, string message = null)
|
||||
{
|
||||
var job = new Job
|
||||
{
|
||||
OpId = GenerateOpId(),
|
||||
Type = type,
|
||||
Status = JobStatus.Pending,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Message = message ?? $"{type} operation started",
|
||||
Progress = 0.0f
|
||||
};
|
||||
|
||||
lock (_jobsLock)
|
||||
{
|
||||
_jobs[job.OpId] = job;
|
||||
}
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a job by op_id.
|
||||
/// </summary>
|
||||
public static Job GetJob(string opId)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.TryGetValue(opId, out var job) ? job : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job status to Complete.
|
||||
/// </summary>
|
||||
public static void CompleteJob(string opId, string message = null, object data = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Status = JobStatus.Complete;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
job.Progress = 1.0f;
|
||||
if (message != null) job.Message = message;
|
||||
if (data != null) job.Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job status to Error.
|
||||
/// </summary>
|
||||
public static void FailJob(string opId, string errorMessage)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Status = JobStatus.Error;
|
||||
job.CompletedAt = DateTime.UtcNow;
|
||||
job.ErrorMessage = errorMessage;
|
||||
job.Message = $"Operation failed: {errorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates job progress (0.0 to 1.0).
|
||||
/// </summary>
|
||||
public static void UpdateProgress(string opId, float progress, string message = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
job.Progress = Mathf.Clamp01(progress);
|
||||
if (message != null) job.Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a job from tracking.
|
||||
/// </summary>
|
||||
public static void RemoveJob(string opId)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
_jobs.Remove(opId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all pending jobs of a specific type.
|
||||
/// </summary>
|
||||
public static List<Job> GetPendingJobs(JobType? type = null)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.Values
|
||||
.Where(j => j.Status == JobStatus.Pending && (!type.HasValue || j.Type == type.Value))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up old jobs that have been completed or timed out.
|
||||
/// Should be called periodically.
|
||||
/// </summary>
|
||||
public static void CleanupOldJobs(int maxAgeSeconds = 3600)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddSeconds(-maxAgeSeconds);
|
||||
|
||||
lock (_jobsLock)
|
||||
{
|
||||
var toRemove = _jobs
|
||||
.Where(kv => kv.Value.CompletedAt.HasValue && kv.Value.CompletedAt.Value < cutoff)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var opId in toRemove)
|
||||
{
|
||||
_jobs.Remove(opId);
|
||||
}
|
||||
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
Debug.Log($"[AsyncOperationTracker] Cleaned up {toRemove.Count} old jobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a job has timed out.
|
||||
/// </summary>
|
||||
public static bool IsJobTimedOut(string opId, int timeoutSeconds = DefaultTimeoutSeconds)
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
if (_jobs.TryGetValue(opId, out var job))
|
||||
{
|
||||
if (job.Status == JobStatus.Pending)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - job.CreatedAt).TotalSeconds;
|
||||
return elapsed > timeoutSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Pending async operation response for a job.
|
||||
/// Protocol: status/poll_interval/op_id
|
||||
/// </summary>
|
||||
public static object CreatePendingResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "pending",
|
||||
["poll_interval"] = 1.0, // Poll every 1 second
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = true,
|
||||
["message"] = job.Message,
|
||||
["data"] = new
|
||||
{
|
||||
type = job.Type.ToString(),
|
||||
progress = job.Progress,
|
||||
createdAt = job.CreatedAt.ToString("o")
|
||||
}
|
||||
};
|
||||
|
||||
// Add operations state delta showing the new pending operation
|
||||
var opDelta = StateComposer.CreateOperationsDelta(new[] {
|
||||
new { id = job.OpId, type = job.Type.ToString(), progress = job.Progress, message = job.Message }
|
||||
});
|
||||
response["state_delta"] = stateDelta != null
|
||||
? StateComposer.MergeStateDeltas(opDelta, stateDelta)
|
||||
: opDelta;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Complete async operation response for a job.
|
||||
/// Protocol: status/op_id
|
||||
/// </summary>
|
||||
public static object CreateCompleteResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "complete",
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = true,
|
||||
["message"] = job.Message,
|
||||
["data"] = job.Data
|
||||
};
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Error async operation response for a job.
|
||||
/// Protocol: status/op_id
|
||||
/// </summary>
|
||||
public static object CreateErrorResponse(Job job, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
["status"] = "error",
|
||||
["op_id"] = job.OpId,
|
||||
["success"] = false,
|
||||
["message"] = job.Message,
|
||||
["error"] = job.ErrorMessage
|
||||
};
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique operation ID.
|
||||
/// </summary>
|
||||
private static string GenerateOpId()
|
||||
{
|
||||
return Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets count of all jobs by status.
|
||||
/// </summary>
|
||||
public static Dictionary<JobStatus, int> GetJobCounts()
|
||||
{
|
||||
lock (_jobsLock)
|
||||
{
|
||||
return _jobs.Values
|
||||
.GroupBy(j => j.Status)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ccdab4cb337ac174395ef110bfa8f3b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for handling binary framed TCP communication
|
||||
/// </summary>
|
||||
public static class BinaryFrameHelper
|
||||
{
|
||||
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
|
||||
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
|
||||
|
||||
/// <summary>
|
||||
/// Timeout-aware exact read helper with cancellation; avoids indefinite stalls and background task leaks
|
||||
/// </summary>
|
||||
public static async Task<byte[]> ReadExactAsync(NetworkStream stream, int count, int timeoutMs, CancellationToken cancel = default)
|
||||
{
|
||||
byte[] buffer = new byte[count];
|
||||
int offset = 0;
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
while (offset < count)
|
||||
{
|
||||
int remaining = count - offset;
|
||||
int remainingTimeout = timeoutMs <= 0
|
||||
? Timeout.Infinite
|
||||
: timeoutMs - (int)stopwatch.ElapsedMilliseconds;
|
||||
|
||||
// If a finite timeout is configured and already elapsed, fail immediately
|
||||
if (remainingTimeout != Timeout.Infinite && remainingTimeout <= 0)
|
||||
{
|
||||
throw new System.IO.IOException("Read timed out");
|
||||
}
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
|
||||
if (remainingTimeout != Timeout.Infinite)
|
||||
{
|
||||
cts.CancelAfter(remainingTimeout);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
#if NETSTANDARD2_1 || NET6_0_OR_GREATER
|
||||
int read = await stream.ReadAsync(buffer.AsMemory(offset, remaining), cts.Token).ConfigureAwait(false);
|
||||
#else
|
||||
int read = await stream.ReadAsync(buffer, offset, remaining, cts.Token).ConfigureAwait(false);
|
||||
#endif
|
||||
if (read == 0)
|
||||
{
|
||||
throw new System.IO.IOException("Connection closed before reading expected bytes");
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancel.IsCancellationRequested)
|
||||
{
|
||||
throw new System.IO.IOException("Read timed out");
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a framed payload to the stream with default timeout
|
||||
/// </summary>
|
||||
public static async Task WriteFrameAsync(NetworkStream stream, byte[] payload)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(FrameIOTimeoutMs);
|
||||
await WriteFrameAsync(stream, payload, cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a framed payload to the stream with cancellation support
|
||||
/// </summary>
|
||||
public static async Task WriteFrameAsync(NetworkStream stream, byte[] payload, CancellationToken cancel)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(payload));
|
||||
}
|
||||
if ((ulong)payload.LongLength > MaxFrameBytes)
|
||||
{
|
||||
throw new System.IO.IOException($"Frame too large: {payload.LongLength}");
|
||||
}
|
||||
byte[] header = new byte[8];
|
||||
WriteUInt64BigEndian(header, (ulong)payload.LongLength);
|
||||
#if NETSTANDARD2_1 || NET6_0_OR_GREATER
|
||||
await stream.WriteAsync(header.AsMemory(0, header.Length), cancel).ConfigureAwait(false);
|
||||
await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancel).ConfigureAwait(false);
|
||||
#else
|
||||
await stream.WriteAsync(header, 0, header.Length, cancel).ConfigureAwait(false);
|
||||
await stream.WriteAsync(payload, 0, payload.Length, cancel).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a framed UTF-8 string from the stream
|
||||
/// </summary>
|
||||
public static async Task<string> ReadFrameAsUtf8Async(NetworkStream stream, int timeoutMs, CancellationToken cancel)
|
||||
{
|
||||
byte[] header = await ReadExactAsync(stream, 8, timeoutMs, cancel).ConfigureAwait(false);
|
||||
ulong payloadLen = ReadUInt64BigEndian(header);
|
||||
if (payloadLen > MaxFrameBytes)
|
||||
{
|
||||
throw new System.IO.IOException($"Invalid framed length: {payloadLen}");
|
||||
}
|
||||
if (payloadLen == 0UL)
|
||||
throw new System.IO.IOException("Zero-length frames are not allowed");
|
||||
if (payloadLen > int.MaxValue)
|
||||
{
|
||||
throw new System.IO.IOException("Frame too large for buffer");
|
||||
}
|
||||
int count = (int)payloadLen;
|
||||
byte[] payload = await ReadExactAsync(stream, count, timeoutMs, cancel).ConfigureAwait(false);
|
||||
return System.Text.Encoding.UTF8.GetString(payload);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a UInt64 from a byte array in big-endian format
|
||||
/// </summary>
|
||||
public static ulong ReadUInt64BigEndian(byte[] buffer)
|
||||
{
|
||||
if (buffer == null || buffer.Length < 8) return 0UL;
|
||||
return ((ulong)buffer[0] << 56)
|
||||
| ((ulong)buffer[1] << 48)
|
||||
| ((ulong)buffer[2] << 40)
|
||||
| ((ulong)buffer[3] << 32)
|
||||
| ((ulong)buffer[4] << 24)
|
||||
| ((ulong)buffer[5] << 16)
|
||||
| ((ulong)buffer[6] << 8)
|
||||
| buffer[7];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a UInt64 to a byte array in big-endian format
|
||||
/// </summary>
|
||||
public static void WriteUInt64BigEndian(byte[] dest, ulong value)
|
||||
{
|
||||
if (dest == null || dest.Length < 8)
|
||||
{
|
||||
throw new System.ArgumentException("Destination buffer too small for UInt64");
|
||||
}
|
||||
dest[0] = (byte)(value >> 56);
|
||||
dest[1] = (byte)(value >> 48);
|
||||
dest[2] = (byte)(value >> 40);
|
||||
dest[3] = (byte)(value >> 32);
|
||||
dest[4] = (byte)(value >> 24);
|
||||
dest[5] = (byte)(value >> 16);
|
||||
dest[6] = (byte)(value >> 8);
|
||||
dest[7] = (byte)(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9addeba67f678854eada157f672b975d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,232 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for Unity compilation status checking and error tracking
|
||||
/// </summary>
|
||||
public static class CompilationHelper
|
||||
{
|
||||
// Track last known compilation error/warning counts
|
||||
// IMPORTANT: Keep these nullable. Returning 0 when counts are unknown is misleading
|
||||
// (it can be interpreted as "validated: no errors/warnings").
|
||||
private static int? _lastErrorCount = null;
|
||||
private static int? _lastWarningCount = null;
|
||||
private static bool _trackingInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to check compilation status across Unity versions
|
||||
/// </summary>
|
||||
public static bool IsCompiling()
|
||||
{
|
||||
if (EditorApplication.isCompiling)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
try
|
||||
{
|
||||
System.Type pipeline = System.Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor");
|
||||
var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (prop != null)
|
||||
{
|
||||
return (bool)prop.GetValue(null);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of compilation errors from the console.
|
||||
/// This is an approximation based on console log entries.
|
||||
/// </summary>
|
||||
public static int? GetCompilationErrors()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get error count from LogEntries (internal API)
|
||||
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
||||
if (logEntriesType != null)
|
||||
{
|
||||
var getCountMethod = logEntriesType.GetMethod(
|
||||
"GetCount",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
// Get count with error filter (mode = 1 for errors)
|
||||
var getCountByTypeMethod = logEntriesType.GetMethod(
|
||||
"GetCountsByType",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
if (getCountByTypeMethod != null)
|
||||
{
|
||||
// GetCountsByType returns counts for errors, warnings, logs
|
||||
var counts = new int[3];
|
||||
getCountByTypeMethod.Invoke(null, new object[] { counts });
|
||||
_lastErrorCount = counts[0]; // Errors
|
||||
return _lastErrorCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[CompilationHelper] Failed to get error count: {e.Message}");
|
||||
}
|
||||
|
||||
return _lastErrorCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of compilation warnings from the console.
|
||||
/// This is an approximation based on console log entries.
|
||||
/// </summary>
|
||||
public static int? GetCompilationWarnings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to get warning count from LogEntries (internal API)
|
||||
var logEntriesType = typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries");
|
||||
if (logEntriesType != null)
|
||||
{
|
||||
var getCountByTypeMethod = logEntriesType.GetMethod(
|
||||
"GetCountsByType",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
||||
);
|
||||
|
||||
if (getCountByTypeMethod != null)
|
||||
{
|
||||
// GetCountsByType returns counts for errors, warnings, logs
|
||||
var counts = new int[3];
|
||||
getCountByTypeMethod.Invoke(null, new object[] { counts });
|
||||
_lastWarningCount = counts[1]; // Warnings
|
||||
return _lastWarningCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[CompilationHelper] Failed to get warning count: {e.Message}");
|
||||
}
|
||||
|
||||
return _lastWarningCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets tracked error/warning counts.
|
||||
/// Should be called before starting a new compilation.
|
||||
/// </summary>
|
||||
public static void ResetCounts()
|
||||
{
|
||||
_lastErrorCount = null;
|
||||
_lastWarningCount = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a standard compilation pipeline:
|
||||
/// 1. Clears console and gets since_token
|
||||
/// 2. Requests compilation
|
||||
/// 3. Returns pending response with token for later log reading
|
||||
///
|
||||
/// This is the recommended pattern after any script modification.
|
||||
/// </summary>
|
||||
public static object StartCompilationPipeline()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Step 1: Clear console and get since_token
|
||||
var clearMethod = typeof(UnityTcp.Editor.Tools.ReadConsole).GetMethod(
|
||||
"HandleCommand",
|
||||
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public
|
||||
);
|
||||
|
||||
string sinceToken = null;
|
||||
if (clearMethod != null)
|
||||
{
|
||||
var clearParams = new Codely.Newtonsoft.Json.Linq.JObject
|
||||
{
|
||||
["action"] = "clear"
|
||||
};
|
||||
var clearResult = clearMethod.Invoke(null, new object[] { clearParams });
|
||||
|
||||
// Extract since_token from result
|
||||
if (clearResult != null)
|
||||
{
|
||||
var resultType = clearResult.GetType();
|
||||
var dataProp = resultType.GetProperty("data");
|
||||
if (dataProp != null)
|
||||
{
|
||||
var data = dataProp.GetValue(clearResult);
|
||||
if (data != null)
|
||||
{
|
||||
var tokenProp = data.GetType().GetProperty("sinceToken");
|
||||
sinceToken = tokenProp?.GetValue(data)?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: get token from StateComposer
|
||||
if (string.IsNullOrEmpty(sinceToken))
|
||||
{
|
||||
sinceToken = StateComposer.GetCurrentConsoleToken();
|
||||
}
|
||||
|
||||
// Step 2: Reset error counts
|
||||
ResetCounts();
|
||||
|
||||
// Step 3: Create compilation job
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.Compilation,
|
||||
"Script compilation pipeline started"
|
||||
);
|
||||
|
||||
// Step 4: Request compilation
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
|
||||
|
||||
// Step 5: Return pending response with token and structured pipeline hints
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as System.Collections.Generic.Dictionary<string, object>;
|
||||
if (response != null)
|
||||
{
|
||||
response["since_token"] = sinceToken;
|
||||
response["pipeline"] = new
|
||||
{
|
||||
step = "compiling",
|
||||
sinceToken = sinceToken
|
||||
};
|
||||
response["pipeline_kind"] = "compile";
|
||||
response["requires_console_validation"] = true;
|
||||
}
|
||||
|
||||
return response ?? AsyncOperationTracker.CreatePendingResponse(job);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[CompilationHelper] StartCompilationPipeline failed: {e}");
|
||||
return Response.Error($"Failed to start compilation pipeline: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary of the last compilation result.
|
||||
/// </summary>
|
||||
public static object GetCompilationSummary()
|
||||
{
|
||||
var errors = GetCompilationErrors();
|
||||
var warnings = GetCompilationWarnings();
|
||||
|
||||
// Only include fields that are actually known; returning 0 is misleading.
|
||||
var result = new System.Collections.Generic.Dictionary<string, object>
|
||||
{
|
||||
["isCompiling"] = IsCompiling()
|
||||
};
|
||||
|
||||
if (errors.HasValue) result["errors"] = errors.Value;
|
||||
if (warnings.HasValue) result["warnings"] = warnings.Value;
|
||||
if (errors.HasValue) result["success"] = errors.Value == 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96c791cc231905b4ca2231ba84bc1f4f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/ExecPath.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
internal static class ExecPath
|
||||
{
|
||||
private const string PrefClaude = "UnityTcp.ClaudeCliPath";
|
||||
|
||||
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
|
||||
internal static string ResolveClaude()
|
||||
{
|
||||
try
|
||||
{
|
||||
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
|
||||
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
|
||||
}
|
||||
catch { }
|
||||
|
||||
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
|
||||
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/opt/homebrew/bin/claude",
|
||||
"/usr/local/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
|
||||
string nvmClaude = ResolveClaudeFromNvm(home);
|
||||
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
// Common npm global locations
|
||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
// Prefer .cmd (most reliable from non-interactive processes)
|
||||
Path.Combine(appData, "npm", "claude.cmd"),
|
||||
Path.Combine(localAppData, "npm", "claude.cmd"),
|
||||
// Fall back to PowerShell shim if only .ps1 is present
|
||||
Path.Combine(appData, "npm", "claude.ps1"),
|
||||
Path.Combine(localAppData, "npm", "claude.ps1"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
|
||||
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
// Linux
|
||||
{
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/usr/local/bin/claude",
|
||||
"/usr/bin/claude",
|
||||
Path.Combine(home, ".local", "bin", "claude"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
|
||||
string nvmClaude = ResolveClaudeFromNvm(home);
|
||||
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
|
||||
private static string ResolveClaudeFromNvm(string home)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(home)) return null;
|
||||
string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
|
||||
if (!Directory.Exists(nvmNodeDir)) return null;
|
||||
|
||||
string bestPath = null;
|
||||
Version bestVersion = null;
|
||||
foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
|
||||
{
|
||||
string name = Path.GetFileName(versionDir);
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
|
||||
string versionStr = name.Substring(1);
|
||||
int dashIndex = versionStr.IndexOf('-');
|
||||
if (dashIndex > 0)
|
||||
{
|
||||
versionStr = versionStr.Substring(0, dashIndex);
|
||||
}
|
||||
if (Version.TryParse(versionStr, out Version parsed))
|
||||
{
|
||||
string candidate = Path.Combine(versionDir, "bin", "claude");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
if (bestVersion == null || parsed > bestVersion)
|
||||
{
|
||||
bestVersion = parsed;
|
||||
bestPath = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestPath;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// Explicitly set the Claude CLI absolute path override in EditorPrefs
|
||||
internal static void SetClaudeCliPath(string absolutePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
|
||||
{
|
||||
EditorPrefs.SetString(PrefClaude, absolutePath);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Clear any previously set Claude CLI override path
|
||||
internal static void ClearClaudeCliPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (EditorPrefs.HasKey(PrefClaude))
|
||||
{
|
||||
EditorPrefs.DeleteKey(PrefClaude);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
internal static bool TryRun(
|
||||
string file,
|
||||
string args,
|
||||
string workingDir,
|
||||
out string stdout,
|
||||
out string stderr,
|
||||
int timeoutMs = 15000,
|
||||
string extraPathPrepend = null)
|
||||
{
|
||||
stdout = string.Empty;
|
||||
stderr = string.Empty;
|
||||
try
|
||||
{
|
||||
// Handle PowerShell scripts on Windows by invoking through powershell.exe
|
||||
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
|
||||
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = isPs1 ? "powershell.exe" : file,
|
||||
Arguments = isPs1
|
||||
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
|
||||
: args,
|
||||
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
if (!string.IsNullOrEmpty(extraPathPrepend))
|
||||
{
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
|
||||
? extraPathPrepend
|
||||
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
|
||||
|
||||
var so = new StringBuilder();
|
||||
var se = new StringBuilder();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
|
||||
|
||||
if (!process.Start()) return false;
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
if (!process.WaitForExit(timeoutMs))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure async buffers are flushed
|
||||
process.WaitForExit();
|
||||
|
||||
stdout = so.ToString();
|
||||
stderr = se.ToString();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
private static string Which(string exe, string prependPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("/usr/bin/which", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
|
||||
using var p = Process.Start(psi);
|
||||
string output = p?.StandardOutput.ReadToEnd().Trim();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private static string Where(string exe)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("where", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
using var p = Process.Start(psi);
|
||||
string first = p?.StandardOutput.ReadToEnd()
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4a955ce7b85e184597177720c6d46b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,536 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Serialization; // For Converters
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles serialization of GameObjects and Components for MCP responses.
|
||||
/// Includes reflection helpers and caching for performance.
|
||||
/// </summary>
|
||||
public static class GameObjectSerializer
|
||||
{
|
||||
// --- Data Serialization ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a GameObject.
|
||||
/// </summary>
|
||||
public static object GetGameObjectData(GameObject go)
|
||||
{
|
||||
if (go == null)
|
||||
return null;
|
||||
return new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceID(),
|
||||
tag = go.tag,
|
||||
layer = go.layer,
|
||||
activeSelf = go.activeSelf,
|
||||
activeInHierarchy = go.activeInHierarchy,
|
||||
isStatic = go.isStatic,
|
||||
scenePath = go.scene.path, // Identify which scene it belongs to
|
||||
transform = new // Serialize transform components carefully to avoid JSON issues
|
||||
{
|
||||
// Serialize Vector3 components individually to prevent self-referencing loops.
|
||||
// The default serializer can struggle with properties like Vector3.normalized.
|
||||
position = new
|
||||
{
|
||||
x = go.transform.position.x,
|
||||
y = go.transform.position.y,
|
||||
z = go.transform.position.z,
|
||||
},
|
||||
localPosition = new
|
||||
{
|
||||
x = go.transform.localPosition.x,
|
||||
y = go.transform.localPosition.y,
|
||||
z = go.transform.localPosition.z,
|
||||
},
|
||||
rotation = new
|
||||
{
|
||||
x = go.transform.rotation.eulerAngles.x,
|
||||
y = go.transform.rotation.eulerAngles.y,
|
||||
z = go.transform.rotation.eulerAngles.z,
|
||||
},
|
||||
localRotation = new
|
||||
{
|
||||
x = go.transform.localRotation.eulerAngles.x,
|
||||
y = go.transform.localRotation.eulerAngles.y,
|
||||
z = go.transform.localRotation.eulerAngles.z,
|
||||
},
|
||||
scale = new
|
||||
{
|
||||
x = go.transform.localScale.x,
|
||||
y = go.transform.localScale.y,
|
||||
z = go.transform.localScale.z,
|
||||
},
|
||||
forward = new
|
||||
{
|
||||
x = go.transform.forward.x,
|
||||
y = go.transform.forward.y,
|
||||
z = go.transform.forward.z,
|
||||
},
|
||||
up = new
|
||||
{
|
||||
x = go.transform.up.x,
|
||||
y = go.transform.up.y,
|
||||
z = go.transform.up.z,
|
||||
},
|
||||
right = new
|
||||
{
|
||||
x = go.transform.right.x,
|
||||
y = go.transform.right.y,
|
||||
z = go.transform.right.z,
|
||||
},
|
||||
},
|
||||
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
|
||||
// Optionally include components, but can be large
|
||||
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
|
||||
// Or just component names:
|
||||
componentNames = go.GetComponents<Component>()
|
||||
.Select(c => c.GetType().FullName)
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Metadata Caching for Reflection ---
|
||||
private class CachedMetadata
|
||||
{
|
||||
public readonly List<PropertyInfo> SerializableProperties;
|
||||
public readonly List<FieldInfo> SerializableFields;
|
||||
|
||||
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
|
||||
{
|
||||
SerializableProperties = properties;
|
||||
SerializableFields = fields;
|
||||
}
|
||||
}
|
||||
// Key becomes Tuple<Type, bool>
|
||||
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
|
||||
// --- End Metadata Caching ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a Component, attempting to serialize
|
||||
/// public properties and fields using reflection, with caching and control over non-public fields.
|
||||
/// </summary>
|
||||
// Add the flag parameter here
|
||||
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
|
||||
{
|
||||
// --- Add Early Logging ---
|
||||
// Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})");
|
||||
// --- End Early Logging ---
|
||||
|
||||
if (c == null) return null;
|
||||
Type componentType = c.GetType();
|
||||
|
||||
// --- Special handling for Transform to avoid reflection crashes and problematic properties ---
|
||||
if (componentType == typeof(Transform))
|
||||
{
|
||||
Transform tr = c as Transform;
|
||||
// Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})");
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", tr.GetInstanceID() },
|
||||
// Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
|
||||
{ "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles
|
||||
{ "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
|
||||
{ "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 },
|
||||
{ "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 },
|
||||
{ "childCount", tr.childCount },
|
||||
// Include standard Object/Component properties
|
||||
{ "name", tr.name },
|
||||
{ "tag", tr.tag },
|
||||
{ "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 }
|
||||
};
|
||||
}
|
||||
// --- End Special handling for Transform ---
|
||||
|
||||
// --- Special handling for Camera to avoid matrix-related crashes ---
|
||||
if (componentType == typeof(Camera))
|
||||
{
|
||||
Camera cam = c as Camera;
|
||||
var cameraProperties = new Dictionary<string, object>();
|
||||
|
||||
// List of safe properties to serialize
|
||||
var safeProperties = new Dictionary<string, Func<object>>
|
||||
{
|
||||
{ "nearClipPlane", () => cam.nearClipPlane },
|
||||
{ "farClipPlane", () => cam.farClipPlane },
|
||||
{ "fieldOfView", () => cam.fieldOfView },
|
||||
{ "renderingPath", () => (int)cam.renderingPath },
|
||||
{ "actualRenderingPath", () => (int)cam.actualRenderingPath },
|
||||
{ "allowHDR", () => cam.allowHDR },
|
||||
{ "allowMSAA", () => cam.allowMSAA },
|
||||
{ "allowDynamicResolution", () => cam.allowDynamicResolution },
|
||||
{ "forceIntoRenderTexture", () => cam.forceIntoRenderTexture },
|
||||
{ "orthographicSize", () => cam.orthographicSize },
|
||||
{ "orthographic", () => cam.orthographic },
|
||||
{ "opaqueSortMode", () => (int)cam.opaqueSortMode },
|
||||
{ "transparencySortMode", () => (int)cam.transparencySortMode },
|
||||
{ "depth", () => cam.depth },
|
||||
{ "aspect", () => cam.aspect },
|
||||
{ "cullingMask", () => cam.cullingMask },
|
||||
{ "eventMask", () => cam.eventMask },
|
||||
{ "backgroundColor", () => cam.backgroundColor },
|
||||
{ "clearFlags", () => (int)cam.clearFlags },
|
||||
{ "stereoEnabled", () => cam.stereoEnabled },
|
||||
{ "stereoSeparation", () => cam.stereoSeparation },
|
||||
{ "stereoConvergence", () => cam.stereoConvergence },
|
||||
{ "enabled", () => cam.enabled },
|
||||
{ "name", () => cam.name },
|
||||
{ "tag", () => cam.tag },
|
||||
{ "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } }
|
||||
};
|
||||
|
||||
foreach (var prop in safeProperties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = prop.Value();
|
||||
if (value != null)
|
||||
{
|
||||
AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silently skip any property that fails
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", cam.GetInstanceID() },
|
||||
{ "properties", cameraProperties }
|
||||
};
|
||||
}
|
||||
// --- End Special handling for Camera ---
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", c.GetInstanceID() }
|
||||
};
|
||||
|
||||
// --- Get Cached or Generate Metadata (using new cache key) ---
|
||||
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
|
||||
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
|
||||
{
|
||||
var propertiesToCache = new List<PropertyInfo>();
|
||||
var fieldsToCache = new List<FieldInfo>();
|
||||
|
||||
// Traverse the hierarchy from the component type up to MonoBehaviour
|
||||
Type currentType = componentType;
|
||||
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
|
||||
{
|
||||
// Get properties declared only at the current type level
|
||||
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var propInfo in currentType.GetProperties(propFlags))
|
||||
{
|
||||
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
|
||||
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
|
||||
// Add if not already added (handles overrides - keep the most derived version)
|
||||
if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
|
||||
propertiesToCache.Add(propInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get fields declared only at the current type level (both public and non-public)
|
||||
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
var declaredFields = currentType.GetFields(fieldFlags);
|
||||
|
||||
// Process the declared Fields for caching
|
||||
foreach (var fieldInfo in declaredFields)
|
||||
{
|
||||
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
|
||||
|
||||
// Add if not already added (handles hiding - keep the most derived version)
|
||||
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
|
||||
|
||||
bool shouldInclude = false;
|
||||
if (includeNonPublicSerializedFields)
|
||||
{
|
||||
// If TRUE, include Public OR NonPublic with [SerializeField]
|
||||
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
|
||||
}
|
||||
else // includeNonPublicSerializedFields is FALSE
|
||||
{
|
||||
// If FALSE, include ONLY if it is explicitly Public.
|
||||
shouldInclude = fieldInfo.IsPublic;
|
||||
}
|
||||
|
||||
if (shouldInclude)
|
||||
{
|
||||
fieldsToCache.Add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the base type
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
// --- End Hierarchy Traversal ---
|
||||
|
||||
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
|
||||
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
|
||||
}
|
||||
// --- End Get Cached or Generate Metadata ---
|
||||
|
||||
// --- Use cached metadata ---
|
||||
var serializablePropertiesOutput = new Dictionary<string, object>();
|
||||
|
||||
// --- Add Logging Before Property Loop ---
|
||||
// Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}...");
|
||||
// --- End Logging Before Property Loop ---
|
||||
|
||||
// Use cached properties
|
||||
foreach (var propInfo in cachedData.SerializableProperties)
|
||||
{
|
||||
string propName = propInfo.Name;
|
||||
|
||||
// --- Skip known obsolete/problematic Component shortcut properties ---
|
||||
bool skipProperty = false;
|
||||
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
|
||||
propName == "light" || propName == "animation" || propName == "constantForce" ||
|
||||
propName == "renderer" || propName == "audio" || propName == "networkView" ||
|
||||
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
|
||||
propName == "particleSystem" ||
|
||||
// Also skip potentially problematic Matrix properties prone to cycles/errors
|
||||
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Generic Properties ---
|
||||
|
||||
// --- Skip Renderer.material / materials to avoid instantiating materials in edit mode ---
|
||||
if (!skipProperty &&
|
||||
(typeof(Renderer).IsAssignableFrom(componentType)) &&
|
||||
(propName == "material" || propName == "materials"))
|
||||
{
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End skip Renderer material properties ---
|
||||
|
||||
// --- Skip specific potentially problematic Camera properties ---
|
||||
if (componentType == typeof(Camera) &&
|
||||
(propName == "pixelRect" ||
|
||||
propName == "rect" ||
|
||||
propName == "cullingMatrix" ||
|
||||
propName == "useOcclusionCulling" ||
|
||||
propName == "worldToCameraMatrix" ||
|
||||
propName == "projectionMatrix" ||
|
||||
propName == "nonJitteredProjectionMatrix" ||
|
||||
propName == "previousViewProjectionMatrix" ||
|
||||
propName == "cameraToWorldMatrix"))
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}");
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Camera Properties ---
|
||||
|
||||
// --- Skip specific potentially problematic Transform properties ---
|
||||
if (componentType == typeof(Transform) &&
|
||||
(propName == "lossyScale" ||
|
||||
propName == "rotation" ||
|
||||
propName == "worldToLocalMatrix" ||
|
||||
propName == "localToWorldMatrix"))
|
||||
{
|
||||
// Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}");
|
||||
skipProperty = true;
|
||||
}
|
||||
// --- End Skip Transform Properties ---
|
||||
|
||||
// Skip if flagged
|
||||
if (skipProperty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// --- Add detailed logging ---
|
||||
// Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}");
|
||||
// --- End detailed logging ---
|
||||
object value = propInfo.GetValue(c);
|
||||
Type propType = propInfo.PropertyType;
|
||||
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Debug.LogWarning($"Could not read property {propName} on {componentType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Add Logging Before Field Loop ---
|
||||
// Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}...");
|
||||
// --- End Logging Before Field Loop ---
|
||||
|
||||
// Use cached fields
|
||||
foreach (var fieldInfo in cachedData.SerializableFields)
|
||||
{
|
||||
try
|
||||
{
|
||||
// --- Add detailed logging for fields ---
|
||||
// Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}");
|
||||
// --- End detailed logging for fields ---
|
||||
object value = fieldInfo.GetValue(c);
|
||||
string fieldName = fieldInfo.Name;
|
||||
Type fieldType = fieldInfo.FieldType;
|
||||
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}");
|
||||
}
|
||||
}
|
||||
// --- End Use cached metadata ---
|
||||
|
||||
if (serializablePropertiesOutput.Count > 0)
|
||||
{
|
||||
data["properties"] = serializablePropertiesOutput;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper function to decide how to serialize different types
|
||||
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
|
||||
{
|
||||
// Simplified: Directly use CreateTokenFromValue which uses the serializer
|
||||
if (value == null)
|
||||
{
|
||||
dict[name] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use the helper that employs our custom serializer settings
|
||||
JToken token = CreateTokenFromValue(value, type);
|
||||
if (token != null) // Check if serialization succeeded in the helper
|
||||
{
|
||||
// Convert JToken back to a basic object structure for the dictionary
|
||||
dict[name] = ConvertJTokenToPlainObject(token);
|
||||
}
|
||||
// If token is null, it means serialization failed and a warning was logged.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch potential errors during JToken conversion or addition to dictionary
|
||||
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert JToken back to basic object structure
|
||||
private static object ConvertJTokenToPlainObject(JToken token)
|
||||
{
|
||||
if (token == null) return null;
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
var objDict = new Dictionary<string, object>();
|
||||
foreach (var prop in ((JObject)token).Properties())
|
||||
{
|
||||
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
|
||||
}
|
||||
return objDict;
|
||||
|
||||
case JTokenType.Array:
|
||||
var list = new List<object>();
|
||||
foreach (var item in (JArray)token)
|
||||
{
|
||||
list.Add(ConvertJTokenToPlainObject(item));
|
||||
}
|
||||
return list;
|
||||
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<long>(); // Use long for safety
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<double>(); // Use double for safety
|
||||
case JTokenType.String:
|
||||
return token.ToObject<string>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Date:
|
||||
return token.ToObject<DateTime>();
|
||||
case JTokenType.Guid:
|
||||
return token.ToObject<Guid>();
|
||||
case JTokenType.Uri:
|
||||
return token.ToObject<Uri>();
|
||||
case JTokenType.TimeSpan:
|
||||
return token.ToObject<TimeSpan>();
|
||||
case JTokenType.Bytes:
|
||||
return token.ToObject<byte[]>();
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
case JTokenType.Undefined:
|
||||
return null; // Treat undefined as null
|
||||
|
||||
default:
|
||||
// Fallback for simple value types not explicitly listed
|
||||
if (token is JValue jValue && jValue.Value != null)
|
||||
{
|
||||
return jValue.Value;
|
||||
}
|
||||
// Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Define custom JsonSerializerSettings for OUTPUT ---
|
||||
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Vector3Converter(),
|
||||
new Vector2Converter(),
|
||||
new QuaternionConverter(),
|
||||
new ColorConverter(),
|
||||
new RectConverter(),
|
||||
new BoundsConverter(),
|
||||
new UnityEngineObjectConverter() // Handles serialization of references
|
||||
},
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
|
||||
};
|
||||
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
|
||||
// --- End Define custom JsonSerializerSettings ---
|
||||
|
||||
// Helper to create JToken using the output serializer
|
||||
private static JToken CreateTokenFromValue(object value, Type type)
|
||||
{
|
||||
if (value == null) return JValue.CreateNull();
|
||||
|
||||
try
|
||||
{
|
||||
// Use the pre-configured OUTPUT serializer instance
|
||||
return JToken.FromObject(value, _outputSerializer);
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Codely.Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
catch (Exception e) // Catch other unexpected errors
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29be5222623c0324ca01f6b7ffaaa602
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for JSON command processing utilities
|
||||
/// </summary>
|
||||
public static class JsonCommandHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper method to check if a string is valid JSON
|
||||
/// </summary>
|
||||
public static bool IsValidJson(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
text = text.Trim();
|
||||
if (
|
||||
(text.StartsWith("{") && text.EndsWith("}"))
|
||||
|| // Object
|
||||
(text.StartsWith("[") && text.EndsWith("]"))
|
||||
) // Array
|
||||
{
|
||||
try
|
||||
{
|
||||
JToken.Parse(text);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get a summary of parameters for error reporting
|
||||
/// </summary>
|
||||
public static string GetParamsSummary(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
return @params == null || !@params.HasValues
|
||||
? "No parameters"
|
||||
: string.Join(
|
||||
", ",
|
||||
@params
|
||||
.Properties()
|
||||
.Select(static p =>
|
||||
$"{p.Name}: {p.Value?.ToString()?[..Math.Min(20, p.Value?.ToString()?.Length ?? 0)]}"
|
||||
)
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Could not summarize parameters";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a17149005eb51642ab32aa65be61cc7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for main thread operations
|
||||
/// </summary>
|
||||
public static class MainThreadHelper
|
||||
{
|
||||
private static int mainThreadId;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the main thread ID for safe thread checks
|
||||
/// Call this from the main thread during static constructor
|
||||
/// </summary>
|
||||
public static void InitializeMainThreadId()
|
||||
{
|
||||
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the given function on the Unity main thread and wait up to timeoutMs for the result.
|
||||
/// Returns null on timeout or error; caller should provide a fallback error response.
|
||||
/// </summary>
|
||||
public static object InvokeOnMainThreadWithTimeout(Func<object> func, int timeoutMs)
|
||||
{
|
||||
if (func == null) return null;
|
||||
try
|
||||
{
|
||||
// If mainThreadId is unknown, assume we're on main thread to avoid blocking the editor.
|
||||
if (mainThreadId == 0)
|
||||
{
|
||||
try { return func(); }
|
||||
catch (Exception ex) { throw new InvalidOperationException($"Main thread handler error: {ex.Message}", ex); }
|
||||
}
|
||||
// If we are already on the main thread, execute directly to avoid deadlocks
|
||||
try
|
||||
{
|
||||
if (Thread.CurrentThread.ManagedThreadId == mainThreadId)
|
||||
{
|
||||
return func();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
object result = null;
|
||||
Exception captured = null;
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
result = func();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
captured = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { tcs.TrySetResult(true); } catch { }
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for completion with timeout (Editor thread will pump delayCall)
|
||||
bool completed = tcs.Task.Wait(timeoutMs);
|
||||
if (!completed)
|
||||
{
|
||||
return null; // timeout
|
||||
}
|
||||
if (captured != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Main thread handler error: {captured.Message}", captured);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to invoke on main thread: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f24f49a4ec33ffe448b5c69d381dfa9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
479
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/PortManager.cs
Normal file
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages dynamic port allocation and persistent storage for Codely Bridge connections
|
||||
/// </summary>
|
||||
public static class PortManager
|
||||
{
|
||||
private static bool IsDebugEnabled()
|
||||
{
|
||||
try { return EditorPrefs.GetBool("UnityTcp.DebugLogs", false); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private const int DefaultPort = 25916;
|
||||
private const int MaxPortAttempts = 100;
|
||||
private const string RegistryFileName = ".com-unity-codely.json";
|
||||
|
||||
[Serializable]
|
||||
public class PortConfig
|
||||
{
|
||||
public int unity_port;
|
||||
public string created_date;
|
||||
public string project_path;
|
||||
|
||||
// Status/heartbeat fields
|
||||
public bool reloading;
|
||||
public string reason;
|
||||
public int seq;
|
||||
public string last_heartbeat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the port to use - either from storage or discover a new one
|
||||
/// Will try stored port first, then fallback to discovering new port
|
||||
/// </summary>
|
||||
/// <returns>Port number to use</returns>
|
||||
public static int GetPortWithFallback()
|
||||
{
|
||||
// Try to load stored port first, but only if it's from the current project
|
||||
var storedConfig = GetStoredPortConfig();
|
||||
if (storedConfig != null &&
|
||||
storedConfig.unity_port > 0 &&
|
||||
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsPortAvailable(storedConfig.unity_port))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Using stored port {storedConfig.unity_port} for current project");
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
|
||||
// If stored port exists but is currently busy, wait briefly for release
|
||||
if (storedConfig != null && storedConfig.unity_port > 0)
|
||||
{
|
||||
if (WaitForPortRelease(storedConfig.unity_port, 1500))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
|
||||
return storedConfig.unity_port;
|
||||
}
|
||||
|
||||
// If no valid stored port, find a new one and save it
|
||||
int newPort = FindAvailablePort();
|
||||
SavePort(newPort);
|
||||
return newPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover and save a new available port (used by Auto-Connect button)
|
||||
/// </summary>
|
||||
/// <returns>New available port</returns>
|
||||
public static int DiscoverNewPort()
|
||||
{
|
||||
int newPort = FindAvailablePort();
|
||||
SavePort(newPort);
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Discovered and saved new port: {newPort}");
|
||||
return newPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an available port starting from the default port
|
||||
/// </summary>
|
||||
/// <returns>Available port number</returns>
|
||||
private static int FindAvailablePort()
|
||||
{
|
||||
// Always try default port first
|
||||
if (IsPortAvailable(DefaultPort))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Using default port {DefaultPort}");
|
||||
return DefaultPort;
|
||||
}
|
||||
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Default port {DefaultPort} is in use, searching for alternative...");
|
||||
|
||||
// Search for alternatives
|
||||
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
|
||||
{
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Found available port {port}");
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific port is available for binding
|
||||
/// Uses same socket options as the actual TCP listener to ensure consistent behavior
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>True if port is available</returns>
|
||||
public static bool IsPortAvailable(int port)
|
||||
{
|
||||
TcpListener testListener = null;
|
||||
try
|
||||
{
|
||||
testListener = new TcpListener(IPAddress.Loopback, port);
|
||||
|
||||
// Use same socket options as the actual listener for consistent checking
|
||||
#if UNITY_EDITOR_WIN
|
||||
// On Windows: no reuse + exclusive access for strict isolation
|
||||
testListener.Server.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress,
|
||||
false
|
||||
);
|
||||
try
|
||||
{
|
||||
testListener.ExclusiveAddressUse = true; // Require exclusive access for availability check
|
||||
}
|
||||
catch { }
|
||||
#else
|
||||
// On macOS/Linux: Disable port reuse
|
||||
try
|
||||
{
|
||||
testListener.Server.SetSocketOption(
|
||||
SocketOptionLevel.Socket,
|
||||
SocketOptionName.ReuseAddress,
|
||||
false
|
||||
);
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
|
||||
// Minimize TIME_WAIT by sending RST on close (same as actual listener)
|
||||
try
|
||||
{
|
||||
testListener.Server.LingerState = new LingerOption(true, 0);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore if not supported on platform
|
||||
}
|
||||
|
||||
testListener.Start();
|
||||
testListener.Stop();
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { testListener?.Stop(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a port is currently being used by Codely Bridge server
|
||||
/// This helps avoid unnecessary port changes when Unity itself is using the port
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>True if port appears to be used by Codely Bridge server</returns>
|
||||
public static bool IsPortUsedByUnityTcp(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to make a quick connection to see if it's a Codely Bridge server
|
||||
using var client = new TcpClient();
|
||||
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
|
||||
if (connectTask.Wait(100)) // 100ms timeout
|
||||
{
|
||||
// If connection succeeded, it's likely the Codely Bridge server
|
||||
return client.Connected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect if another Codely Bridge instance is already using this port
|
||||
/// Provides better error reporting for port conflicts
|
||||
/// </summary>
|
||||
/// <param name="port">Port to check</param>
|
||||
/// <returns>Detailed information about port usage</returns>
|
||||
public static (bool inUse, string description) CheckPortConflict(int port)
|
||||
{
|
||||
// First check basic availability with exclusive access
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
return (false, "Port is available");
|
||||
}
|
||||
|
||||
// Port is in use, try to determine what's using it
|
||||
if (IsPortUsedByUnityTcp(port))
|
||||
{
|
||||
return (true, "Port is in use by another Codely Bridge Bridge instance");
|
||||
}
|
||||
|
||||
// Port is in use by something else
|
||||
return (true, "Port is in use by another process");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for a port to become available for a limited amount of time.
|
||||
/// Used to bridge the gap during domain reload when the old listener
|
||||
/// hasn't released the socket yet.
|
||||
/// </summary>
|
||||
private static bool WaitForPortRelease(int port, int timeoutMs)
|
||||
{
|
||||
int waited = 0;
|
||||
const int step = 100;
|
||||
while (waited < timeoutMs)
|
||||
{
|
||||
if (IsPortAvailable(port))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the port is in use by a Codely Bridge instance, continue waiting briefly
|
||||
if (!IsPortUsedByUnityTcp(port))
|
||||
{
|
||||
// In use by something else; don't keep waiting
|
||||
return false;
|
||||
}
|
||||
|
||||
Thread.Sleep(step);
|
||||
waited += step;
|
||||
}
|
||||
return IsPortAvailable(port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save port to persistent storage, preserving existing status information
|
||||
/// </summary>
|
||||
/// <param name="port">Port to save</param>
|
||||
private static void SavePort(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load existing config to preserve status information
|
||||
var existingConfig = GetStoredPortConfig();
|
||||
|
||||
var portConfig = new PortConfig
|
||||
{
|
||||
unity_port = port,
|
||||
created_date = existingConfig?.created_date ?? DateTime.UtcNow.ToString("O"),
|
||||
project_path = Application.dataPath,
|
||||
|
||||
// Preserve existing status fields
|
||||
reloading = existingConfig?.reloading ?? false,
|
||||
reason = existingConfig?.reason ?? "ready",
|
||||
seq = existingConfig?.seq ?? 0,
|
||||
last_heartbeat = existingConfig?.last_heartbeat
|
||||
};
|
||||
|
||||
SavePortConfig(portConfig);
|
||||
|
||||
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>Codely Bridge</color></b>: Saved port {port} to storage");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not save port to storage: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save port configuration to persistent storage
|
||||
/// </summary>
|
||||
/// <param name="portConfig">Port configuration to save</param>
|
||||
public static void SavePortConfig(PortConfig portConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryDir = GetRegistryDirectory();
|
||||
Directory.CreateDirectory(registryDir);
|
||||
|
||||
string registryFile = GetRegistryFilePath();
|
||||
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
|
||||
// Write to project root config file
|
||||
File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
|
||||
|
||||
// Also maintain backwards compatibility by writing to legacy location
|
||||
try
|
||||
{
|
||||
string legacyDir = GetLegacyRegistryDirectory();
|
||||
Directory.CreateDirectory(legacyDir);
|
||||
string legacyFile = Path.Combine(legacyDir, "unity-tcp-port.json");
|
||||
File.WriteAllText(legacyFile, json, new System.Text.UTF8Encoding(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore legacy write failures
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not save port config to storage: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load port from persistent storage
|
||||
/// </summary>
|
||||
/// <returns>Stored port number, or 0 if not found</returns>
|
||||
private static int LoadStoredPort()
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryFile = GetRegistryFilePath();
|
||||
|
||||
if (!File.Exists(registryFile))
|
||||
{
|
||||
// Backwards compatibility: try the legacy locations
|
||||
// First try the new legacy location in project root
|
||||
string projectLegacy = Path.Combine(GetRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(projectLegacy))
|
||||
{
|
||||
registryFile = projectLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Then try the old user home location
|
||||
string userHomeLegacy = Path.Combine(GetLegacyRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(userHomeLegacy))
|
||||
{
|
||||
registryFile = userHomeLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also check hash-based files in user home
|
||||
string hashBased = Path.Combine(GetLegacyRegistryDirectory(), $"unity-tcp-port-{ComputeProjectHash(Application.dataPath)}.json");
|
||||
if (File.Exists(hashBased))
|
||||
{
|
||||
registryFile = hashBased;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(registryFile);
|
||||
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
|
||||
|
||||
return portConfig?.unity_port ?? 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not load port from storage: {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current stored port configuration
|
||||
/// </summary>
|
||||
/// <returns>Port configuration if exists, null otherwise</returns>
|
||||
public static PortConfig GetStoredPortConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
string registryFile = GetRegistryFilePath();
|
||||
|
||||
if (!File.Exists(registryFile))
|
||||
{
|
||||
// Backwards compatibility: try the legacy locations
|
||||
// First try the new legacy location in project root
|
||||
string projectLegacy = Path.Combine(GetRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(projectLegacy))
|
||||
{
|
||||
registryFile = projectLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Then try the old user home location
|
||||
string userHomeLegacy = Path.Combine(GetLegacyRegistryDirectory(), "unity-tcp-port.json");
|
||||
if (File.Exists(userHomeLegacy))
|
||||
{
|
||||
registryFile = userHomeLegacy;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also check hash-based files in user home
|
||||
string hashBased = Path.Combine(GetLegacyRegistryDirectory(), $"unity-tcp-port-{ComputeProjectHash(Application.dataPath)}.json");
|
||||
if (File.Exists(hashBased))
|
||||
{
|
||||
registryFile = hashBased;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(registryFile);
|
||||
return JsonConvert.DeserializeObject<PortConfig>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not load port config: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegistryDirectory()
|
||||
{
|
||||
// Use project root directory (parent of Assets folder)
|
||||
string assetsPath = Application.dataPath;
|
||||
string projectRoot = Directory.GetParent(assetsPath)?.FullName ?? assetsPath;
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
private static string GetLegacyRegistryDirectory()
|
||||
{
|
||||
// Legacy location in user home directory
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-tcp");
|
||||
}
|
||||
|
||||
private static string GetRegistryFilePath()
|
||||
{
|
||||
string dir = GetRegistryDirectory();
|
||||
return Path.Combine(dir, RegistryFileName);
|
||||
}
|
||||
|
||||
private static string ComputeProjectHash(string input)
|
||||
{
|
||||
try
|
||||
{
|
||||
using SHA1 sha1 = SHA1.Create();
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
|
||||
byte[] hashBytes = sha1.ComputeHash(bytes);
|
||||
var sb = new StringBuilder();
|
||||
foreach (byte b in hashBytes)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
return sb.ToString()[..8]; // short, sufficient for filenames
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db27ee87f1c170b47928576e31cc2c9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/Response.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides static methods for creating standardized success and error response objects.
|
||||
/// Ensures consistent JSON structure for communication back to the Codely client.
|
||||
///
|
||||
/// Response format aligns with the OpenAPI spec:
|
||||
/// - ImmediateResponse: { success, message, data?, state?, state_delta? }
|
||||
/// - PendingResponse: { _mcp_status, op_id, poll_interval, message, state?, state_delta? }
|
||||
/// </summary>
|
||||
public static class Response
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a standardized success response object with optional state.
|
||||
/// </summary>
|
||||
/// <param name="message">A message describing the successful operation.</param>
|
||||
/// <param name="data">Optional additional data to include in the response.</param>
|
||||
/// <param name="includeState">Whether to include full state snapshot (default: false).</param>
|
||||
/// <param name="stateDelta">Optional state delta for incremental updates.</param>
|
||||
/// <returns>An object representing the success response.</returns>
|
||||
public static object Success(string message, object data = null, bool includeState = false, object stateDelta = null)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
{ "success", true },
|
||||
{ "message", message },
|
||||
// Always include current state revision so clients can keep client_state_rev in sync
|
||||
{ "rev", StateComposer.GetCurrentRevision() }
|
||||
};
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
response["data"] = data;
|
||||
}
|
||||
|
||||
// Include state if explicitly requested
|
||||
if (includeState)
|
||||
{
|
||||
response["state"] = StateComposer.BuildFullState();
|
||||
}
|
||||
|
||||
// Include state_delta if provided
|
||||
if (stateDelta != null)
|
||||
{
|
||||
response["state_delta"] = stateDelta;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized success response with automatic state delta.
|
||||
/// Use this for write operations that modify Unity state.
|
||||
/// </summary>
|
||||
public static object SuccessWithDelta(string message, object data = null, object stateDelta = null)
|
||||
{
|
||||
StateComposer.IncrementRevision();
|
||||
return Success(message, data, includeState: false, stateDelta: stateDelta);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized success response with full state snapshot.
|
||||
/// Use this for operations that require the client to have the latest state.
|
||||
/// </summary>
|
||||
public static object SuccessWithState(string message, object data = null)
|
||||
{
|
||||
StateComposer.IncrementRevision();
|
||||
return Success(message, data, includeState: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a standardized error response object.
|
||||
/// </summary>
|
||||
/// <param name="errorCodeOrMessage">A message describing the error.</param>
|
||||
/// <param name="data">Optional additional data (e.g., error details) to include.</param>
|
||||
/// <param name="includeState">Whether to include full state snapshot for recovery (default: false).</param>
|
||||
/// <returns>An object representing the error response.</returns>
|
||||
public static object Error(string errorCodeOrMessage, object data = null, bool includeState = false)
|
||||
{
|
||||
var response = new Dictionary<string, object>
|
||||
{
|
||||
{ "success", false },
|
||||
{ "code", errorCodeOrMessage },
|
||||
{ "error", errorCodeOrMessage }
|
||||
};
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
response["data"] = data;
|
||||
}
|
||||
|
||||
// Include state on error for recovery scenarios
|
||||
if (includeState)
|
||||
{
|
||||
response["state"] = StateComposer.BuildFullState();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a conflict response for state revision mismatches.
|
||||
/// This is returned when client_state_rev doesn't match server's revision.
|
||||
/// </summary>
|
||||
/// <param name="clientRev">The client's provided revision.</param>
|
||||
/// <param name="serverRev">The server's current revision.</param>
|
||||
/// <returns>A conflict response with full state for synchronization.</returns>
|
||||
public static object Conflict(int clientRev, int serverRev)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "success", false },
|
||||
{ "code", "state_revision_conflict" },
|
||||
{ "error", $"State revision mismatch. Client: {clientRev}, Server: {serverRev}. Please refresh state." },
|
||||
{ "state", StateComposer.BuildFullState() }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy overload for backward compatibility.
|
||||
/// </summary>
|
||||
[Obsolete("Use Success(message, data, includeState, stateDelta) instead.")]
|
||||
public static object SuccessLegacy(string message, object data = null)
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = message,
|
||||
data = data,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new { success = true, message = message };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 635f93405037a114993e3a9a44c54745
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
public static class ServerPathResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to locate the package root directory for cn.tuanjie.codely.bridge.
|
||||
/// Returns true if found and sets packagePath to the package root folder.
|
||||
/// </summary>
|
||||
public static bool TryFindPackageRoot(out string packagePath, bool warnOnLegacyPackageId = true)
|
||||
{
|
||||
// Resolve via local package info (no network). Fall back to Client.List on older editors.
|
||||
try
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
// Primary: the package that owns this assembly
|
||||
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
|
||||
if (owner != null)
|
||||
{
|
||||
if (TryResolvePackage(owner, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary: scan all registered packages locally
|
||||
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
|
||||
{
|
||||
if (TryResolvePackage(p, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Older Unity versions: use Package Manager Client.List as a fallback
|
||||
var list = UnityEditor.PackageManager.Client.List();
|
||||
while (!list.IsCompleted) { }
|
||||
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
|
||||
{
|
||||
foreach (var pkg in list.Result)
|
||||
{
|
||||
if (TryResolvePackage(pkg, out packagePath, warnOnLegacyPackageId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
packagePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolvePackage(UnityEditor.PackageManager.PackageInfo p, out string packagePath, bool warnOnLegacyPackageId)
|
||||
{
|
||||
const string CurrentId = "cn.tuanjie.codely.bridge";
|
||||
|
||||
packagePath = null;
|
||||
if (p == null || p.name != CurrentId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
packagePath = p.resolvedPath;
|
||||
return !string.IsNullOrEmpty(packagePath) && Directory.Exists(packagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,783 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized state composition and revision tracking for Unity Editor state.
|
||||
/// Provides consistent state snapshots and incremental state_delta generation.
|
||||
/// </summary>
|
||||
public static class StateComposer
|
||||
{
|
||||
// Global state revision counter (incremented on every state change)
|
||||
private static int _globalRevision = 0;
|
||||
private static readonly object _revisionLock = new object();
|
||||
|
||||
// Console state tracking (shared with ReadConsole)
|
||||
private static string _currentConsoleToken = null;
|
||||
private static int _consoleUnreadCount = 0;
|
||||
private static readonly List<object> _lastConsoleErrors = new List<object>();
|
||||
private static readonly object _consoleLock = new object();
|
||||
|
||||
// Touched assets tracking
|
||||
private static readonly List<object> _touchedAssets = new List<object>();
|
||||
private static readonly object _assetsLock = new object();
|
||||
|
||||
// Pending operations tracking
|
||||
private static readonly List<object> _pendingOperations = new List<object>();
|
||||
private static readonly object _operationsLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Increment and return the next global revision number.
|
||||
/// Thread-safe.
|
||||
/// </summary>
|
||||
public static int IncrementRevision()
|
||||
{
|
||||
lock (_revisionLock)
|
||||
{
|
||||
return ++_globalRevision;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current global revision without incrementing.
|
||||
/// </summary>
|
||||
public static int GetCurrentRevision()
|
||||
{
|
||||
lock (_revisionLock)
|
||||
{
|
||||
return _globalRevision;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complete Unity state snapshot with current revision.
|
||||
/// Note: Does NOT auto-increment revision - caller should decide when to increment.
|
||||
/// </summary>
|
||||
public static object BuildFullState()
|
||||
{
|
||||
int currentRev;
|
||||
lock (_revisionLock)
|
||||
{
|
||||
currentRev = _globalRevision;
|
||||
}
|
||||
|
||||
var state = new
|
||||
{
|
||||
editor = BuildEditorState(),
|
||||
project = BuildProjectState(),
|
||||
scene = BuildSceneState(),
|
||||
selection = BuildSelectionState(),
|
||||
console = BuildConsoleState(),
|
||||
assets = BuildAssetsState(),
|
||||
operations = BuildOperationsState(),
|
||||
policy = BuildPolicyState(),
|
||||
rev = currentRev
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complete Unity state snapshot and increments revision.
|
||||
/// Use this for read operations that need to return fresh state.
|
||||
/// </summary>
|
||||
public static object BuildFullStateAndIncrement()
|
||||
{
|
||||
int newRev = IncrementRevision();
|
||||
|
||||
var state = new
|
||||
{
|
||||
editor = BuildEditorState(),
|
||||
project = BuildProjectState(),
|
||||
scene = BuildSceneState(),
|
||||
selection = BuildSelectionState(),
|
||||
console = BuildConsoleState(),
|
||||
assets = BuildAssetsState(),
|
||||
operations = BuildOperationsState(),
|
||||
policy = BuildPolicyState(),
|
||||
rev = newRev
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds editor-specific state.
|
||||
/// </summary>
|
||||
public static object BuildEditorState()
|
||||
{
|
||||
var playMode = EditorApplication.isPlaying ? "playing" :
|
||||
(EditorApplication.isPaused ? "paused" : "stopped");
|
||||
|
||||
// Get focused window
|
||||
string focusedWindow = null;
|
||||
if (EditorWindow.focusedWindow != null)
|
||||
{
|
||||
focusedWindow = EditorWindow.focusedWindow.GetType().Name;
|
||||
}
|
||||
|
||||
// Determine if operations require focus
|
||||
// This is a heuristic - some operations need the editor to be focused
|
||||
bool requiresFocusForOperations = DetermineIfFocusRequired();
|
||||
|
||||
return new
|
||||
{
|
||||
playMode = playMode,
|
||||
focusedWindow = focusedWindow,
|
||||
requiresFocusForOperations = requiresFocusForOperations,
|
||||
isCompiling = EditorApplication.isCompiling,
|
||||
isUpdating = EditorApplication.isUpdating,
|
||||
lastCompilation = BuildLastCompilationState(),
|
||||
timeSinceStartup = (float)EditorApplication.timeSinceStartup
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds last compilation state.
|
||||
///
|
||||
/// NOTE:
|
||||
/// - This is intentionally minimal and only reports whether Unity is
|
||||
/// currently compiling ("started" vs "idle").
|
||||
/// - It is NOT a per-compilation snapshot and does NOT expose error/
|
||||
/// warning counts for any specific pipeline.
|
||||
/// - For accurate diagnostics (including error/warning counts), callers
|
||||
/// must use:
|
||||
/// * Compilation deltas from StateComposer.CreateCompilationDelta
|
||||
/// (returned by wait_for_compile), and
|
||||
/// * The Unity console (read_console / unity_console) with sinceToken.
|
||||
/// </summary>
|
||||
private static object BuildLastCompilationState()
|
||||
{
|
||||
var status = EditorApplication.isCompiling ? "started" : "idle";
|
||||
|
||||
return new
|
||||
{
|
||||
status = status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if current operations require focus.
|
||||
/// </summary>
|
||||
private static bool DetermineIfFocusRequired()
|
||||
{
|
||||
// Heuristic: Some operations need focus, especially during Play mode
|
||||
// or when performing visual operations like scene manipulation
|
||||
if (EditorApplication.isPlaying || EditorApplication.isPaused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if SceneView needs focus for certain operations
|
||||
var sceneView = EditorWindow.focusedWindow as SceneView;
|
||||
if (sceneView != null)
|
||||
{
|
||||
return false; // Already focused
|
||||
}
|
||||
|
||||
return false; // Default: focus not strictly required
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds project-specific state.
|
||||
/// </summary>
|
||||
public static object BuildProjectState()
|
||||
{
|
||||
// Detect Render Pipeline
|
||||
string srp = "builtin";
|
||||
var currentRP = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
|
||||
if (currentRP != null)
|
||||
{
|
||||
string rpName = currentRP.GetType().Name.ToLowerInvariant();
|
||||
if (rpName.Contains("urp") || rpName.Contains("universal"))
|
||||
{
|
||||
srp = "urp";
|
||||
}
|
||||
else if (rpName.Contains("hdrp") || rpName.Contains("highdefinition"))
|
||||
{
|
||||
srp = "hdrp";
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
srp = srp,
|
||||
defineSymbols = GetScriptingDefineSymbols(),
|
||||
packages = GetInstalledPackages(),
|
||||
dirty = false // Would track if project settings are modified
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GetScriptingDefineSymbols()
|
||||
{
|
||||
// Get scripting define symbols for current build target
|
||||
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
|
||||
var symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup);
|
||||
return string.IsNullOrEmpty(symbols) ?
|
||||
new string[0] :
|
||||
symbols.Split(';', StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private static string[] GetInstalledPackages()
|
||||
{
|
||||
// Simplified - in production would use PackageManager API
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds scene-specific state.
|
||||
/// </summary>
|
||||
public static object BuildSceneState()
|
||||
{
|
||||
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
|
||||
|
||||
return new
|
||||
{
|
||||
activeScenePath = activeScene.path,
|
||||
dirty = activeScene.isDirty,
|
||||
hasNavMeshData = HasNavMeshData(),
|
||||
hasLightingData = HasLightingData()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasNavMeshData()
|
||||
{
|
||||
// Check if current scene has NavMesh data using runtime reflection
|
||||
try
|
||||
{
|
||||
// First, try to check NavMeshSurface components (com.unity.ai.navigation package)
|
||||
Type navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation");
|
||||
if (navMeshSurfaceType == null)
|
||||
{
|
||||
// Fallback: search in loaded assemblies
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
navMeshSurfaceType = assembly.GetType("Unity.AI.Navigation.NavMeshSurface");
|
||||
if (navMeshSurfaceType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (navMeshSurfaceType != null)
|
||||
{
|
||||
// Check NavMeshSurface components for navMeshData
|
||||
var activeSurfacesProperty = navMeshSurfaceType.GetProperty("activeSurfaces", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (activeSurfacesProperty != null)
|
||||
{
|
||||
var activeSurfaces = activeSurfacesProperty.GetValue(null);
|
||||
if (activeSurfaces is System.Collections.IList surfaceList && surfaceList.Count > 0)
|
||||
{
|
||||
var navMeshDataProperty = navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
foreach (var surface in surfaceList)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check all NavMeshSurface components in the scene (including inactive)
|
||||
var allSurfaces = Resources.FindObjectsOfTypeAll(navMeshSurfaceType);
|
||||
if (allSurfaces != null && allSurfaces.Length > 0)
|
||||
{
|
||||
var navMeshDataProperty = navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
foreach (var surface in allSurfaces)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to find NavMesh type using reflection (for built-in NavMesh)
|
||||
Type navMeshType = Type.GetType("UnityEngine.AI.NavMesh, UnityEngine.AIModule");
|
||||
if (navMeshType == null)
|
||||
{
|
||||
// Fallback: search in loaded assemblies
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
navMeshType = assembly.GetType("UnityEngine.AI.NavMesh");
|
||||
if (navMeshType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (navMeshType == null)
|
||||
return false;
|
||||
|
||||
// Get CalculateTriangulation method
|
||||
MethodInfo calculateTriangulationMethod = navMeshType.GetMethod("CalculateTriangulation", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
if (calculateTriangulationMethod == null)
|
||||
return false;
|
||||
|
||||
// Call CalculateTriangulation using reflection
|
||||
var triangulation = calculateTriangulationMethod.Invoke(null, null);
|
||||
if (triangulation == null)
|
||||
return false;
|
||||
|
||||
// Get vertices property
|
||||
var verticesProperty = triangulation.GetType().GetProperty("vertices");
|
||||
if (verticesProperty == null)
|
||||
return false;
|
||||
|
||||
var vertices = verticesProperty.GetValue(triangulation) as Array;
|
||||
return vertices != null && vertices.Length > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any error occurs, assume no NavMesh data
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasLightingData()
|
||||
{
|
||||
// Check if current scene has baked lighting
|
||||
return Lightmapping.giWorkflowMode == Lightmapping.GIWorkflowMode.OnDemand ||
|
||||
Lightmapping.lightingDataAsset != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds selection state.
|
||||
/// </summary>
|
||||
public static object BuildSelectionState()
|
||||
{
|
||||
var activeObject = Selection.activeGameObject;
|
||||
object activeObjectInfo = null;
|
||||
|
||||
if (activeObject != null)
|
||||
{
|
||||
activeObjectInfo = new
|
||||
{
|
||||
id = activeObject.GetInstanceID(),
|
||||
name = activeObject.name,
|
||||
hierarchy_path = GetHierarchyPath(activeObject)
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
activeObject = activeObjectInfo
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetHierarchyPath(GameObject go)
|
||||
{
|
||||
if (go == null) return "";
|
||||
|
||||
var path = go.name;
|
||||
var parent = go.transform.parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds console state with real tracking data.
|
||||
/// </summary>
|
||||
public static object BuildConsoleState()
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
return new
|
||||
{
|
||||
sinceToken = _currentConsoleToken,
|
||||
unreadCount = _consoleUnreadCount,
|
||||
lastErrors = _lastConsoleErrors.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates console state tracking. Called by ReadConsole.
|
||||
/// </summary>
|
||||
public static void UpdateConsoleState(string sinceToken, int unreadCount = 0, object[] lastErrors = null)
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
_currentConsoleToken = sinceToken;
|
||||
_consoleUnreadCount = unreadCount;
|
||||
_lastConsoleErrors.Clear();
|
||||
if (lastErrors != null)
|
||||
{
|
||||
_lastConsoleErrors.AddRange(lastErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current console token.
|
||||
/// </summary>
|
||||
public static string GetCurrentConsoleToken()
|
||||
{
|
||||
lock (_consoleLock)
|
||||
{
|
||||
return _currentConsoleToken;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds assets state with tracked touched assets.
|
||||
/// </summary>
|
||||
public static object BuildAssetsState()
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
return new
|
||||
{
|
||||
touched = _touchedAssets.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a touched asset to tracking. Called by asset operations.
|
||||
/// </summary>
|
||||
public static void AddTouchedAsset(string path, bool imported = false, bool hasMeta = true)
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
_touchedAssets.Add(new { path, imported, hasMeta });
|
||||
// Keep only last 100 entries
|
||||
while (_touchedAssets.Count > 100)
|
||||
{
|
||||
_touchedAssets.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears touched assets list.
|
||||
/// </summary>
|
||||
public static void ClearTouchedAssets()
|
||||
{
|
||||
lock (_assetsLock)
|
||||
{
|
||||
_touchedAssets.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds pending operations state from AsyncOperationTracker.
|
||||
/// </summary>
|
||||
public static object BuildOperationsState()
|
||||
{
|
||||
// Get pending operations from AsyncOperationTracker
|
||||
var pendingJobs = AsyncOperationTracker.GetPendingJobs();
|
||||
var pending = pendingJobs.Select(job => new
|
||||
{
|
||||
id = job.OpId,
|
||||
type = job.Type.ToString(),
|
||||
progress = job.Progress,
|
||||
message = job.Message
|
||||
}).ToArray();
|
||||
|
||||
return new
|
||||
{
|
||||
pending = pending
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds policy state.
|
||||
/// </summary>
|
||||
public static object BuildPolicyState()
|
||||
{
|
||||
return new
|
||||
{
|
||||
writeGuardInPlayMode = "deny", // Default: deny writes in Play mode
|
||||
refreshMode = "debounced",
|
||||
consoleReadPolicy = "must_clear_before_read"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Console state delta.
|
||||
/// </summary>
|
||||
public static object CreateConsoleDelta(string sinceToken = null, int? unreadCount = null, object[] lastErrors = null)
|
||||
{
|
||||
var consoleDelta = new Dictionary<string, object>();
|
||||
|
||||
if (sinceToken != null) consoleDelta["sinceToken"] = sinceToken;
|
||||
if (unreadCount.HasValue) consoleDelta["unreadCount"] = unreadCount.Value;
|
||||
if (lastErrors != null) consoleDelta["lastErrors"] = lastErrors;
|
||||
|
||||
return new { console = consoleDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Compilation state delta.
|
||||
/// </summary>
|
||||
public static object CreateCompilationDelta(bool? isCompiling = null, string status = null, int? errors = null, int? warnings = null)
|
||||
{
|
||||
var editorDelta = new Dictionary<string, object>();
|
||||
var compilationDelta = new Dictionary<string, object>();
|
||||
|
||||
if (isCompiling.HasValue) editorDelta["isCompiling"] = isCompiling.Value;
|
||||
|
||||
if (status != null) compilationDelta["status"] = status;
|
||||
if (errors.HasValue) compilationDelta["errors"] = errors.Value;
|
||||
if (warnings.HasValue) compilationDelta["warnings"] = warnings.Value;
|
||||
|
||||
if (compilationDelta.Count > 0)
|
||||
{
|
||||
editorDelta["lastCompilation"] = compilationDelta;
|
||||
}
|
||||
|
||||
return new { editor = editorDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Scene state delta.
|
||||
/// </summary>
|
||||
public static object CreateSceneDelta(string activeScenePath = null, bool? dirty = null)
|
||||
{
|
||||
var sceneDelta = new Dictionary<string, object>();
|
||||
|
||||
if (activeScenePath != null) sceneDelta["activeScenePath"] = activeScenePath;
|
||||
if (dirty.HasValue) sceneDelta["dirty"] = dirty.Value;
|
||||
|
||||
return new { scene = sceneDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Asset state delta.
|
||||
/// </summary>
|
||||
public static object CreateAssetDelta(object[] touchedAssets)
|
||||
{
|
||||
return new
|
||||
{
|
||||
assets = new
|
||||
{
|
||||
touched = touchedAssets
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Editor state delta.
|
||||
/// </summary>
|
||||
public static object CreateEditorDelta(string focusedWindow = null, bool? isUpdating = null)
|
||||
{
|
||||
var editorDelta = new Dictionary<string, object>();
|
||||
|
||||
if (focusedWindow != null) editorDelta["focusedWindow"] = focusedWindow;
|
||||
if (isUpdating.HasValue) editorDelta["isUpdating"] = isUpdating.Value;
|
||||
|
||||
return new { editor = editorDelta };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Operations state delta.
|
||||
/// </summary>
|
||||
public static object CreateOperationsDelta(object[] pendingOperations)
|
||||
{
|
||||
return new
|
||||
{
|
||||
operations = new
|
||||
{
|
||||
pending = pendingOperations
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates client state revision and returns conflict response if mismatched.
|
||||
/// Returns null if validation passes.
|
||||
/// </summary>
|
||||
public static object ValidateClientRevision(int? clientRev)
|
||||
{
|
||||
if (!clientRev.HasValue)
|
||||
{
|
||||
// No client revision provided - accept but don't enforce
|
||||
return null;
|
||||
}
|
||||
|
||||
int currentRev = GetCurrentRevision();
|
||||
if (clientRev.Value != currentRev)
|
||||
{
|
||||
// State mismatch - return 409-like conflict response with fresh state
|
||||
return new
|
||||
{
|
||||
success = false,
|
||||
message = $"State revision mismatch. Client: {clientRev.Value}, Server: {currentRev}. Please refresh state.",
|
||||
code = "state_revision_conflict",
|
||||
state = BuildFullStateAndIncrement()
|
||||
};
|
||||
}
|
||||
|
||||
return null; // Validation passed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates client state revision from JObject params.
|
||||
/// Returns null if validation passes, error response if conflict.
|
||||
/// </summary>
|
||||
public static object ValidateClientRevisionFromParams(Codely.Newtonsoft.Json.Linq.JObject @params)
|
||||
{
|
||||
int? clientRev = @params?["client_state_rev"]?.ToObject<int?>();
|
||||
return ValidateClientRevision(clientRev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple state deltas into one combined delta.
|
||||
/// </summary>
|
||||
public static object MergeStateDeltas(params object[] deltas)
|
||||
{
|
||||
if (deltas == null || deltas.Length == 0) return null;
|
||||
if (deltas.Length == 1) return deltas[0];
|
||||
|
||||
// Preserve legacy behavior: if only one non-null delta is provided, return it as-is.
|
||||
int nonNullCount = 0;
|
||||
object single = null;
|
||||
foreach (var d in deltas)
|
||||
{
|
||||
if (d == null) continue;
|
||||
nonNullCount++;
|
||||
single = d;
|
||||
if (nonNullCount > 1) break;
|
||||
}
|
||||
if (nonNullCount == 0) return null;
|
||||
if (nonNullCount == 1) return single;
|
||||
|
||||
var merged = new Dictionary<string, object>();
|
||||
|
||||
foreach (var delta in deltas)
|
||||
{
|
||||
if (delta == null) continue;
|
||||
|
||||
// Prefer a JSON/dictionary representation to avoid reflection issues
|
||||
// (e.g., when a state_delta is already a JObject/JToken).
|
||||
Dictionary<string, object> deltaDict = null;
|
||||
try
|
||||
{
|
||||
// Codely.Newtonsoft.Json.Linq types (JObject / JToken)
|
||||
if (delta is Codely.Newtonsoft.Json.Linq.JObject jObj)
|
||||
{
|
||||
deltaDict = jObj.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
else if (delta is Codely.Newtonsoft.Json.Linq.JToken jTok &&
|
||||
jTok.Type == Codely.Newtonsoft.Json.Linq.JTokenType.Object)
|
||||
{
|
||||
var asObj = jTok as Codely.Newtonsoft.Json.Linq.JObject;
|
||||
deltaDict = (asObj ?? Codely.Newtonsoft.Json.Linq.JObject.FromObject(jTok))
|
||||
.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
else if (delta is IDictionary<string, object> iDict)
|
||||
{
|
||||
deltaDict = new Dictionary<string, object>(iDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last resort: serialize arbitrary objects into a JObject then into a dictionary.
|
||||
var obj = Codely.Newtonsoft.Json.Linq.JObject.FromObject(delta);
|
||||
deltaDict = obj.ToObject<Dictionary<string, object>>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
deltaDict = null;
|
||||
}
|
||||
|
||||
if (deltaDict != null)
|
||||
{
|
||||
foreach (var kv in deltaDict)
|
||||
{
|
||||
if (kv.Value == null) continue;
|
||||
|
||||
if (merged.ContainsKey(kv.Key))
|
||||
{
|
||||
// Merge nested dictionaries (one level deep, consistent with legacy behavior)
|
||||
var existingDict = merged[kv.Key] as Dictionary<string, object>;
|
||||
var newDict = kv.Value as Dictionary<string, object>;
|
||||
if (existingDict != null && newDict != null)
|
||||
{
|
||||
foreach (var nk in newDict)
|
||||
{
|
||||
existingDict[nk.Key] = nk.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback: reflection-based merge (skip indexer properties to avoid invocation errors)
|
||||
try
|
||||
{
|
||||
var props = delta.GetType().GetProperties();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
if (prop.GetIndexParameters().Length > 0) continue;
|
||||
|
||||
object value = null;
|
||||
try { value = prop.GetValue(delta); } catch { continue; }
|
||||
if (value == null) continue;
|
||||
|
||||
if (merged.ContainsKey(prop.Name))
|
||||
{
|
||||
// Merge nested dictionaries
|
||||
if (merged[prop.Name] is Dictionary<string, object> existingDict &&
|
||||
value is Dictionary<string, object> newDict)
|
||||
{
|
||||
foreach (var kv in newDict)
|
||||
{
|
||||
existingDict[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[prop.Name] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
merged[prop.Name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore merge errors from unexpected delta shapes.
|
||||
}
|
||||
}
|
||||
|
||||
return merged.Count > 0 ? merged : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e6177725a55072419d7584603153d01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Codely.Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for status and heartbeat management
|
||||
/// </summary>
|
||||
public static class StatusHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Write heartbeat status to the main config file
|
||||
/// </summary>
|
||||
public static void WriteHeartbeat(int currentUnityPort, bool reloading, int heartbeatSeq, string reason = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load existing config or create new one
|
||||
var existingConfig = PortManager.GetStoredPortConfig();
|
||||
|
||||
var portConfig = new PortManager.PortConfig
|
||||
{
|
||||
unity_port = currentUnityPort,
|
||||
created_date = existingConfig?.created_date ?? DateTime.UtcNow.ToString("O"),
|
||||
project_path = Application.dataPath,
|
||||
|
||||
// Update status fields
|
||||
reloading = reloading,
|
||||
reason = reason ?? (reloading ? "reloading" : "ready"),
|
||||
seq = heartbeatSeq,
|
||||
last_heartbeat = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
PortManager.SavePortConfig(portConfig);
|
||||
|
||||
// Also maintain backwards compatibility by writing to legacy status location
|
||||
try
|
||||
{
|
||||
// Allow override of status directory (useful in CI/containers)
|
||||
string legacyDir = Environment.GetEnvironmentVariable("UNITY_TCP_STATUS_DIR");
|
||||
if (string.IsNullOrWhiteSpace(legacyDir))
|
||||
{
|
||||
legacyDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-tcp");
|
||||
}
|
||||
Directory.CreateDirectory(legacyDir);
|
||||
string legacyFilePath = Path.Combine(legacyDir, $"unity-tcp-status-{ComputeProjectHash(Application.dataPath)}.json");
|
||||
var legacyPayload = new
|
||||
{
|
||||
unity_port = currentUnityPort,
|
||||
reloading,
|
||||
reason = reason ?? (reloading ? "reloading" : "ready"),
|
||||
seq = heartbeatSeq,
|
||||
project_path = Application.dataPath,
|
||||
last_heartbeat = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
File.WriteAllText(legacyFilePath, JsonConvert.SerializeObject(legacyPayload), new System.Text.UTF8Encoding(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore legacy write failures
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute a short hash of the project path for unique identification
|
||||
/// </summary>
|
||||
public static string ComputeProjectHash(string input)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sha1 = System.Security.Cryptography.SHA1.Create();
|
||||
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty);
|
||||
byte[] hashBytes = sha1.ComputeHash(bytes);
|
||||
var sb = new System.Text.StringBuilder();
|
||||
foreach (byte b in hashBytes)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
return sb.ToString()[..8];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12eba3a4a35da834eba2ca7f63decd97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/TcpLog.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
internal static class TcpLog
|
||||
{
|
||||
private const string Prefix = "<b><color=#2EA3FF>Codely Bridge</color></b>:";
|
||||
|
||||
private static bool IsDebugEnabled()
|
||||
{
|
||||
try { return EditorPrefs.GetBool("UnityTcp.DebugLogs", false); } catch { return false; }
|
||||
}
|
||||
|
||||
public static void Info(string message, bool always = true)
|
||||
{
|
||||
if (!always && !IsDebugEnabled()) return;
|
||||
Debug.Log($"{Prefix} {message}");
|
||||
}
|
||||
|
||||
public static void Warn(string message)
|
||||
{
|
||||
Debug.LogWarning($"<color=#cc7a00>{Prefix} {message}</color>");
|
||||
}
|
||||
|
||||
public static void Error(string message)
|
||||
{
|
||||
Debug.LogError($"<color=#cc3333>{Prefix} {message}</color>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1da5b6ee62708c4a9df5c2a0f624f1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity Bridge telemetry helper for collecting usage analytics
|
||||
/// Following privacy-first approach with easy opt-out mechanisms
|
||||
/// </summary>
|
||||
public static class TelemetryHelper
|
||||
{
|
||||
private const string TELEMETRY_DISABLED_KEY = "UnityTcp.TelemetryDisabled";
|
||||
private const string CUSTOMER_UUID_KEY = "UnityTcp.CustomerUUID";
|
||||
private static Action<Dictionary<string, object>> s_sender;
|
||||
|
||||
/// <summary>
|
||||
/// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
|
||||
/// </summary>
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
// Check environment variables first
|
||||
var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
|
||||
if (!string.IsNullOrEmpty(envDisable) &&
|
||||
(envDisable.ToLower() == "true" || envDisable == "1"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
|
||||
if (!string.IsNullOrEmpty(unityMcpDisable) &&
|
||||
(unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Honor protocol-wide opt-out as well
|
||||
var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY");
|
||||
if (!string.IsNullOrEmpty(mcpDisable) &&
|
||||
(mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check EditorPrefs
|
||||
return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or generate customer UUID for anonymous tracking
|
||||
/// </summary>
|
||||
public static string GetCustomerUUID()
|
||||
{
|
||||
var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
|
||||
if (string.IsNullOrEmpty(uuid))
|
||||
{
|
||||
uuid = System.Guid.NewGuid().ToString();
|
||||
UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable telemetry (stored in EditorPrefs)
|
||||
/// </summary>
|
||||
public static void DisableTelemetry()
|
||||
{
|
||||
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable telemetry (stored in EditorPrefs)
|
||||
/// </summary>
|
||||
public static void EnableTelemetry()
|
||||
{
|
||||
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send telemetry data to Python server for processing
|
||||
/// This is a lightweight bridge - the actual telemetry logic is in Python
|
||||
/// </summary>
|
||||
public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var telemetryData = new Dictionary<string, object>
|
||||
{
|
||||
["event_type"] = eventType,
|
||||
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
["customer_uuid"] = GetCustomerUUID(),
|
||||
["unity_version"] = Application.unityVersion,
|
||||
["platform"] = Application.platform.ToString(),
|
||||
["source"] = "unity_bridge"
|
||||
};
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
telemetryData["data"] = data;
|
||||
}
|
||||
|
||||
// Send to Python server via existing bridge communication
|
||||
// The Python server will handle actual telemetry transmission
|
||||
SendTelemetryToPythonServer(telemetryData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Never let telemetry errors interfere with functionality
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the bridge to register a concrete sender for telemetry payloads.
|
||||
/// </summary>
|
||||
public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
|
||||
{
|
||||
Interlocked.Exchange(ref s_sender, sender);
|
||||
}
|
||||
|
||||
public static void UnregisterTelemetrySender()
|
||||
{
|
||||
Interlocked.Exchange(ref s_sender, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record bridge startup event
|
||||
/// </summary>
|
||||
public static void RecordBridgeStartup()
|
||||
{
|
||||
RecordEvent("bridge_startup", new Dictionary<string, object>
|
||||
{
|
||||
["bridge_version"] = "3.0.2",
|
||||
["auto_connect"] = "unknown" // TODO: we have no such field
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record bridge connection event
|
||||
/// </summary>
|
||||
public static void RecordBridgeConnection(bool success, string error = null)
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["success"] = success
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
data["error"] = error.Substring(0, Math.Min(200, error.Length));
|
||||
}
|
||||
|
||||
RecordEvent("bridge_connection", data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record tool execution from Unity side
|
||||
/// </summary>
|
||||
public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["tool_name"] = toolName,
|
||||
["success"] = success,
|
||||
["duration_ms"] = Math.Round(durationMs, 2)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
data["error"] = error.Substring(0, Math.Min(200, error.Length));
|
||||
}
|
||||
|
||||
RecordEvent("tool_execution_unity", data);
|
||||
}
|
||||
|
||||
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
|
||||
{
|
||||
var sender = Volatile.Read(ref s_sender);
|
||||
if (sender != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
sender(telemetryData);
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: log when debug is enabled
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDebugEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
return UnityEditor.EditorPrefs.GetBool("UnityTcp.DebugLogs", false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75e6d1e08c44ad84d91f82a4baeea37e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Hook mechanism for external tools to notify Unity when they've modified files
|
||||
/// that may affect Unity's state (scripts, assets, scenes, etc.).
|
||||
/// This is NOT exposed as a tool to LLM - it's an internal notification system.
|
||||
/// </summary>
|
||||
public static class UnityStateDirtyHook
|
||||
{
|
||||
/// <summary>
|
||||
/// File change types that can affect Unity state.
|
||||
/// </summary>
|
||||
public enum FileChangeType
|
||||
{
|
||||
ScriptModified, // .cs files modified
|
||||
AssetModified, // Asset files (.prefab, .mat, .asset, etc.) modified
|
||||
SceneModified, // .unity scene files modified
|
||||
ShaderModified, // .shader files modified
|
||||
ConfigModified, // Project settings, package.json, etc. modified
|
||||
UIModified, // .uxml, .uss files modified
|
||||
Unknown // Other file types
|
||||
}
|
||||
|
||||
private static readonly Queue<DirtyNotification> _pendingNotifications = new Queue<DirtyNotification>();
|
||||
private static readonly object _notificationLock = new object();
|
||||
private static bool _refreshScheduled = false;
|
||||
|
||||
/// <summary>
|
||||
/// Notification record for dirty file changes.
|
||||
/// </summary>
|
||||
public class DirtyNotification
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public FileChangeType ChangeType { get; set; }
|
||||
public string FilePath { get; set; }
|
||||
public string ToolName { get; set; }
|
||||
public bool RequiresReimport { get; set; }
|
||||
public bool RequiresCompilation { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by external agentic tools (edit, write, etc.) to notify Unity of file changes.
|
||||
/// This is the main entry point for the hook system.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file that was modified</param>
|
||||
/// <param name="toolName">Name of the tool that made the change (for logging)</param>
|
||||
public static void NotifyFileChanged(string filePath, string toolName = "unknown")
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Normalize path
|
||||
filePath = filePath.Replace('\\', '/');
|
||||
|
||||
// Determine change type and required actions
|
||||
var changeType = DetermineChangeType(filePath);
|
||||
bool requiresReimport = ShouldReimport(filePath, changeType);
|
||||
bool requiresCompilation = RequiresCompilation(filePath, changeType);
|
||||
|
||||
var notification = new DirtyNotification
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
ChangeType = changeType,
|
||||
FilePath = filePath,
|
||||
ToolName = toolName,
|
||||
RequiresReimport = requiresReimport,
|
||||
RequiresCompilation = requiresCompilation
|
||||
};
|
||||
|
||||
lock (_notificationLock)
|
||||
{
|
||||
_pendingNotifications.Enqueue(notification);
|
||||
|
||||
// Schedule refresh on next editor update
|
||||
if (!_refreshScheduled)
|
||||
{
|
||||
_refreshScheduled = true;
|
||||
EditorApplication.delayCall += ProcessPendingNotifications;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[UnityStateDirtyHook] Notified: {changeType} - {filePath} (from {toolName})");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[UnityStateDirtyHook] Failed to process notification for {filePath}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process all pending dirty notifications and trigger appropriate Unity actions.
|
||||
/// </summary>
|
||||
private static void ProcessPendingNotifications()
|
||||
{
|
||||
List<DirtyNotification> toProcess;
|
||||
|
||||
lock (_notificationLock)
|
||||
{
|
||||
if (_pendingNotifications.Count == 0)
|
||||
{
|
||||
_refreshScheduled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
toProcess = new List<DirtyNotification>(_pendingNotifications);
|
||||
_pendingNotifications.Clear();
|
||||
_refreshScheduled = false;
|
||||
}
|
||||
|
||||
// Group by action type
|
||||
var needsReimport = toProcess.Where(n => n.RequiresReimport).Select(n => n.FilePath).Distinct().ToList();
|
||||
var needsCompilation = toProcess.Any(n => n.RequiresCompilation);
|
||||
|
||||
// Process reimports
|
||||
if (needsReimport.Count > 0)
|
||||
{
|
||||
Debug.Log($"[UnityStateDirtyHook] Reimporting {needsReimport.Count} assets...");
|
||||
foreach (var path in needsReimport)
|
||||
{
|
||||
// Convert to Unity-relative path if needed
|
||||
string unityPath = ConvertToUnityPath(path);
|
||||
if (!string.IsNullOrEmpty(unityPath))
|
||||
{
|
||||
AssetDatabase.ImportAsset(unityPath, ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
}
|
||||
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
|
||||
}
|
||||
|
||||
// Increment state revision
|
||||
StateComposer.IncrementRevision();
|
||||
|
||||
// Log summary
|
||||
var summary = new
|
||||
{
|
||||
processed = toProcess.Count,
|
||||
reimported = needsReimport.Count,
|
||||
needsCompilation = needsCompilation,
|
||||
byType = toProcess.GroupBy(n => n.ChangeType).ToDictionary(g => g.Key.ToString(), g => g.Count())
|
||||
};
|
||||
|
||||
Debug.Log($"[UnityStateDirtyHook] Processed notifications: {Codely.Newtonsoft.Json.JsonConvert.SerializeObject(summary)}");
|
||||
|
||||
// If compilation is needed, it will happen automatically via Unity's asset pipeline
|
||||
if (needsCompilation)
|
||||
{
|
||||
Debug.Log("[UnityStateDirtyHook] Script changes detected - Unity will trigger compilation automatically");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the type of change based on file extension.
|
||||
/// </summary>
|
||||
private static FileChangeType DetermineChangeType(string filePath)
|
||||
{
|
||||
string ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant();
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case ".cs":
|
||||
return FileChangeType.ScriptModified;
|
||||
case ".unity":
|
||||
case ".scene":
|
||||
return FileChangeType.SceneModified;
|
||||
case ".shader":
|
||||
case ".shadergraph":
|
||||
case ".shadersubgraph":
|
||||
return FileChangeType.ShaderModified;
|
||||
case ".prefab":
|
||||
case ".mat":
|
||||
case ".asset":
|
||||
case ".png":
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
case ".psd":
|
||||
case ".fbx":
|
||||
case ".obj":
|
||||
case ".mp3":
|
||||
case ".wav":
|
||||
case ".anim":
|
||||
case ".controller":
|
||||
return FileChangeType.AssetModified;
|
||||
case ".uxml":
|
||||
case ".uss":
|
||||
return FileChangeType.UIModified;
|
||||
case ".json":
|
||||
case ".asmdef":
|
||||
case ".asmref":
|
||||
return FileChangeType.ConfigModified;
|
||||
default:
|
||||
return FileChangeType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a file change requires reimporting in Unity.
|
||||
/// </summary>
|
||||
private static bool ShouldReimport(string filePath, FileChangeType changeType)
|
||||
{
|
||||
// Check if file is in Assets/ folder
|
||||
if (!IsInAssetsFolder(filePath))
|
||||
return false;
|
||||
|
||||
switch (changeType)
|
||||
{
|
||||
case FileChangeType.ScriptModified:
|
||||
case FileChangeType.ShaderModified:
|
||||
case FileChangeType.AssetModified:
|
||||
case FileChangeType.SceneModified:
|
||||
case FileChangeType.UIModified:
|
||||
return true;
|
||||
case FileChangeType.ConfigModified:
|
||||
return filePath.Contains("package.json") || filePath.Contains(".asmdef");
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if a file change requires script compilation.
|
||||
/// </summary>
|
||||
private static bool RequiresCompilation(string filePath, FileChangeType changeType)
|
||||
{
|
||||
return changeType == FileChangeType.ScriptModified && IsInAssetsFolder(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a file path is within the Unity Assets folder.
|
||||
/// </summary>
|
||||
private static bool IsInAssetsFolder(string filePath)
|
||||
{
|
||||
string normalizedPath = filePath.Replace('\\', '/');
|
||||
return normalizedPath.Contains("/Assets/") || normalizedPath.StartsWith("Assets/");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an absolute or relative path to Unity-relative path (Assets/...).
|
||||
/// </summary>
|
||||
private static string ConvertToUnityPath(string filePath)
|
||||
{
|
||||
string normalized = filePath.Replace('\\', '/');
|
||||
|
||||
// Already Unity-relative
|
||||
if (normalized.StartsWith("Assets/"))
|
||||
return normalized;
|
||||
|
||||
// Extract Assets/... portion
|
||||
int assetsIndex = normalized.IndexOf("/Assets/");
|
||||
if (assetsIndex >= 0)
|
||||
return normalized.Substring(assetsIndex + 1); // Skip the leading /
|
||||
|
||||
// Check if it's relative to project root
|
||||
string projectRoot = Application.dataPath.Replace("/Assets", "").Replace('\\', '/');
|
||||
if (normalized.StartsWith(projectRoot))
|
||||
{
|
||||
string relativePath = normalized.Substring(projectRoot.Length).TrimStart('/');
|
||||
if (relativePath.StartsWith("Assets/"))
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get statistics about recent dirty notifications (for debugging).
|
||||
/// </summary>
|
||||
public static object GetStatistics()
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
return new
|
||||
{
|
||||
pending = _pendingNotifications.Count,
|
||||
refreshScheduled = _refreshScheduled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all pending notifications (for testing/debugging).
|
||||
/// </summary>
|
||||
public static void ClearPendingNotifications()
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
_pendingNotifications.Clear();
|
||||
_refreshScheduled = false;
|
||||
}
|
||||
Debug.Log("[UnityStateDirtyHook] Cleared all pending notifications");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15dbae89d2b872442a3021294af9f5bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for Vector3 operations
|
||||
/// </summary>
|
||||
public static class Vector3Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a JArray into a Vector3
|
||||
/// </summary>
|
||||
/// <param name="array">The array containing x, y, z coordinates</param>
|
||||
/// <returns>A Vector3 with the parsed coordinates</returns>
|
||||
/// <exception cref="System.Exception">Thrown when array is invalid</exception>
|
||||
public static Vector3 ParseVector3(JArray array)
|
||||
{
|
||||
if (array == null || array.Count != 3)
|
||||
throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z].");
|
||||
return new Vector3((float)array[0], (float)array[1], (float)array[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8514fd42f23cb641a36e52550825b35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
196
Packages/cn.tuanjie.codely.bridge/Editor/Helpers/WriteGuard.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityTcp.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Write protection guard for Unity Editor operations.
|
||||
/// Prevents unsafe modifications during Play mode or other restricted states.
|
||||
/// </summary>
|
||||
public static class WriteGuard
|
||||
{
|
||||
/// <summary>
|
||||
/// Write guard policy enum.
|
||||
/// </summary>
|
||||
public enum Policy
|
||||
{
|
||||
/// <summary>
|
||||
/// Deny all writes in Play/Paused mode (default, safest)
|
||||
/// </summary>
|
||||
Deny,
|
||||
|
||||
/// <summary>
|
||||
/// Allow writes but log warnings (experimental, use with caution)
|
||||
/// </summary>
|
||||
AllowWithWarning
|
||||
}
|
||||
|
||||
// Current policy (default: Deny)
|
||||
private static Policy _currentPolicy = Policy.Deny;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current write guard policy.
|
||||
/// </summary>
|
||||
public static Policy CurrentPolicy
|
||||
{
|
||||
get => _currentPolicy;
|
||||
set => _currentPolicy = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if write operations are allowed in current editor state.
|
||||
/// Returns null if allowed, or an error response object if blocked.
|
||||
/// </summary>
|
||||
/// <param name="operationName">Name of the operation being attempted (for logging)</param>
|
||||
/// <returns>Error response if blocked, null if allowed</returns>
|
||||
public static object CheckWriteAllowed(string operationName = "write operation")
|
||||
{
|
||||
// Check if we're in Play or Paused mode
|
||||
bool inPlayMode = EditorApplication.isPlaying || EditorApplication.isPaused;
|
||||
|
||||
if (!inPlayMode)
|
||||
{
|
||||
// Not in Play mode - writes always allowed
|
||||
return null;
|
||||
}
|
||||
|
||||
// In Play mode - check policy
|
||||
switch (_currentPolicy)
|
||||
{
|
||||
case Policy.Deny:
|
||||
// Block the write and return error
|
||||
string errorMessage = $"Cannot perform {operationName} in Play/Paused mode. " +
|
||||
"Stop Play mode first, or change write guard policy to 'allow_with_warning' (experimental).";
|
||||
|
||||
Debug.LogWarning($"[WriteGuard] {errorMessage}");
|
||||
|
||||
return Response.Error("write_blocked_in_play_mode", new
|
||||
{
|
||||
code = "write_blocked_in_play_mode",
|
||||
message = errorMessage,
|
||||
currentPlayMode = EditorApplication.isPlaying ? "playing" : "paused",
|
||||
policy = "deny",
|
||||
suggestion = "Stop Play mode or change policy to 'allow_with_warning'"
|
||||
});
|
||||
|
||||
case Policy.AllowWithWarning:
|
||||
// Allow but warn
|
||||
string warningMessage = $"[EXPERIMENTAL] Performing {operationName} in Play/Paused mode. " +
|
||||
"This may cause unexpected behavior or data loss!";
|
||||
|
||||
Debug.LogWarning($"[WriteGuard] {warningMessage}");
|
||||
|
||||
// Log to audit trail
|
||||
LogAuditEvent(operationName, "allowed_with_warning");
|
||||
|
||||
// Return null to allow operation
|
||||
return null;
|
||||
|
||||
default:
|
||||
return Response.Error("Invalid write guard policy");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-checks if write operations are blocked (returns true if blocked).
|
||||
/// </summary>
|
||||
public static bool IsWriteBlocked()
|
||||
{
|
||||
if (!EditorApplication.isPlaying && !EditorApplication.isPaused)
|
||||
{
|
||||
return false; // Not in Play mode - not blocked
|
||||
}
|
||||
|
||||
return _currentPolicy == Policy.Deny;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the write guard policy.
|
||||
/// </summary>
|
||||
/// <param name="policy">Policy to set ("deny" or "allow_with_warning")</param>
|
||||
/// <returns>Success or error response</returns>
|
||||
public static object SetPolicy(string policy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(policy))
|
||||
{
|
||||
return Response.Error("Policy parameter is required");
|
||||
}
|
||||
|
||||
string lowerPolicy = policy.ToLowerInvariant();
|
||||
|
||||
switch (lowerPolicy)
|
||||
{
|
||||
case "deny":
|
||||
_currentPolicy = Policy.Deny;
|
||||
Debug.Log("[WriteGuard] Write guard policy set to: Deny");
|
||||
return Response.Success($"Write guard policy set to 'deny'.", new { policy = "deny" });
|
||||
|
||||
case "allow_with_warning":
|
||||
_currentPolicy = Policy.AllowWithWarning;
|
||||
Debug.LogWarning("[WriteGuard] Write guard policy set to: AllowWithWarning (EXPERIMENTAL)");
|
||||
return Response.Success($"Write guard policy set to 'allow_with_warning' (experimental).",
|
||||
new { policy = "allow_with_warning", warning = "This is experimental and may cause issues" });
|
||||
|
||||
default:
|
||||
return Response.Error($"Invalid policy: '{policy}'. Valid policies are: 'deny', 'allow_with_warning'");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current policy as a string.
|
||||
/// </summary>
|
||||
public static string GetPolicyString()
|
||||
{
|
||||
return _currentPolicy == Policy.Deny ? "deny" : "allow_with_warning";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs audit events for write operations in Play mode.
|
||||
/// </summary>
|
||||
private static void LogAuditEvent(string operationName, string action)
|
||||
{
|
||||
// In production, this could write to a file or telemetry system
|
||||
var auditEntry = new
|
||||
{
|
||||
timestamp = DateTime.UtcNow.ToString("o"),
|
||||
operation = operationName,
|
||||
action = action,
|
||||
playMode = EditorApplication.isPlaying ? "playing" : (EditorApplication.isPaused ? "paused" : "stopped"),
|
||||
scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
|
||||
};
|
||||
|
||||
Debug.Log($"[WriteGuard Audit] {Codely.Newtonsoft.Json.JsonConvert.SerializeObject(auditEntry)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a write-blocked error response with detailed information.
|
||||
/// </summary>
|
||||
public static object CreateBlockedResponse(string operationName, string additionalInfo = null)
|
||||
{
|
||||
string message = $"Write operation '{operationName}' blocked in Play/Paused mode.";
|
||||
if (!string.IsNullOrEmpty(additionalInfo))
|
||||
{
|
||||
message += $" {additionalInfo}";
|
||||
}
|
||||
|
||||
return Response.Error("write_blocked_in_play_mode", new
|
||||
{
|
||||
code = "write_blocked_in_play_mode",
|
||||
operation = operationName,
|
||||
currentMode = EditorApplication.isPlaying ? "playing" : "paused",
|
||||
policy = GetPolicyString(),
|
||||
message = message,
|
||||
remediation = new
|
||||
{
|
||||
options = new[]
|
||||
{
|
||||
"Stop Play mode (recommended)",
|
||||
"Change write guard policy to 'allow_with_warning' (experimental, use with caution)"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 593f57416aee1924495c4971200b544b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor/Icons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d6ea1a77c9612d4e951e572b2ce2dbb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/cli.png
Normal file
|
After Width: | Height: | Size: 664 B |
117
Packages/cn.tuanjie.codely.bridge/Editor/Icons/cli.png.meta
Normal file
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6dde9036fcd2a7b4a863cd34c6fe7ab0
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/codely.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
117
Packages/cn.tuanjie.codely.bridge/Editor/Icons/codely.png.meta
Normal file
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7edd824621050a447a76310d055e604a
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/connect.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
117
Packages/cn.tuanjie.codely.bridge/Editor/Icons/connect.png.meta
Normal file
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 01c5e5edc190ab348a87b7c144a18a70
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/connect_hover.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a47b75ced50b68a4e877b83a995b288b
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6931fcb852821d4cb928d1916cce927
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/connecting.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b978c21e8999664ca21dcb42e564385
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/disconnect.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 68c96eb36fea510439ebbd8d7fb58dbf
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2ffef58569b87840a9ce9ae242e9d8b
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a6640d26697e1945a91315a31c97347
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/jetbrains.png
Normal file
|
After Width: | Height: | Size: 724 B |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a257084e1bd2a1748aca50d95944a61f
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/title_icon.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34c51cad0d2c48f4cb8250e55a0106e7
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/tuanjie.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
117
Packages/cn.tuanjie.codely.bridge/Editor/Icons/tuanjie.png.meta
Normal file
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03d8d1c5cc7f4e043a3c6311264621ce
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/unity_editor.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d56ec8f36694b846b917189970885d6
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/visualstudio.png
Normal file
|
After Width: | Height: | Size: 855 B |
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62a56066f603166498bc1fee8767634b
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Packages/cn.tuanjie.codely.bridge/Editor/Icons/vscode.png
Normal file
|
After Width: | Height: | Size: 966 B |
117
Packages/cn.tuanjie.codely.bridge/Editor/Icons/vscode.png.meta
Normal file
@@ -0,0 +1,117 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b16882722be5df8439b6d948a7e68319
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f0829f6381c0ab41b63ef97b48a59cd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,294 @@
|
||||
using Codely.Newtonsoft.Json;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor; // Required for AssetDatabase and EditorUtility
|
||||
#endif
|
||||
|
||||
namespace UnityTcp.Editor.Serialization
|
||||
{
|
||||
public class Vector3Converter : JsonConverter<Vector3>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Vector3(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector2Converter : JsonConverter<Vector2>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Vector2(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class QuaternionConverter : JsonConverter<Quaternion>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WritePropertyName("w");
|
||||
writer.WriteValue(value.w);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Quaternion(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"],
|
||||
(float)jo["w"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorConverter : JsonConverter<Color>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("r");
|
||||
writer.WriteValue(value.r);
|
||||
writer.WritePropertyName("g");
|
||||
writer.WriteValue(value.g);
|
||||
writer.WritePropertyName("b");
|
||||
writer.WriteValue(value.b);
|
||||
writer.WritePropertyName("a");
|
||||
writer.WriteValue(value.a);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Color(
|
||||
(float)jo["r"],
|
||||
(float)jo["g"],
|
||||
(float)jo["b"],
|
||||
(float)jo["a"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class RectConverter : JsonConverter<Rect>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("width");
|
||||
writer.WriteValue(value.width);
|
||||
writer.WritePropertyName("height");
|
||||
writer.WriteValue(value.height);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Rect(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["width"],
|
||||
(float)jo["height"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class BoundsConverter : JsonConverter<Bounds>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("center");
|
||||
serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
|
||||
writer.WritePropertyName("size");
|
||||
serializer.Serialize(writer, value.size); // Use serializer to handle nested Vector3
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
Vector3 size = jo["size"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
return new Bounds(center, size);
|
||||
}
|
||||
}
|
||||
|
||||
// Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
|
||||
public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
|
||||
{
|
||||
public override bool CanRead => true; // We need to implement ReadJson
|
||||
public override bool CanWrite => true;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for finding UnityEngine.Object by instruction (e.g., {"find":"...", "method":"..."}).
|
||||
/// This is set by the Editor assembly at startup to avoid cross-assembly reference issues.
|
||||
/// </summary>
|
||||
public static Func<JObject, Type, UnityEngine.Object> FindObjectByInstruction { get; set; }
|
||||
|
||||
public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
|
||||
if (UnityEditor.AssetDatabase.Contains(value))
|
||||
{
|
||||
// It's an asset (Material, Texture, Prefab, etc.)
|
||||
string path = UnityEditor.AssetDatabase.GetAssetPath(value);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
writer.WriteValue(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Asset exists but path couldn't be found? Write minimal info.
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WritePropertyName("isAssetWithoutPath");
|
||||
writer.WriteValue(true);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's a scene object (GameObject, Component, etc.)
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
#else
|
||||
// Runtime fallback: Write basic info without AssetDatabase
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WritePropertyName("warning");
|
||||
writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
|
||||
writer.WriteEndObject();
|
||||
#endif
|
||||
}
|
||||
|
||||
public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
// Assume it's an asset path
|
||||
string path = reader.Value.ToString();
|
||||
return UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
|
||||
// Handle {"find":"...", "method":"..."} reference lookup format
|
||||
if (jo.TryGetValue("find", out JToken findToken))
|
||||
{
|
||||
if (FindObjectByInstruction != null)
|
||||
{
|
||||
UnityEngine.Object foundObj = FindObjectByInstruction(jo, objectType);
|
||||
if (foundObj != null)
|
||||
{
|
||||
return foundObj;
|
||||
}
|
||||
// Log warning if find instruction was provided but object wasn't found
|
||||
Debug.LogWarning($"Could not find object using instruction: {jo}");
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("FindObjectByInstruction delegate not registered. Cannot resolve find instruction.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = idToken.ToObject<int>();
|
||||
UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
|
||||
if (obj != null && objectType.IsAssignableFrom(obj.GetType()))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
// Could potentially try finding by name as a fallback if ID lookup fails/isn't present
|
||||
// but that's less reliable.
|
||||
}
|
||||
#else
|
||||
// Runtime deserialization is tricky without AssetDatabase/EditorUtility
|
||||
// Maybe log a warning and return null or existingValue?
|
||||
Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
|
||||
// Skip the token to avoid breaking the reader
|
||||
if (reader.TokenType == JsonToken.StartObject) JObject.Load(reader);
|
||||
else if (reader.TokenType == JsonToken.String) reader.ReadAsString();
|
||||
// Return null or existing value, depending on desired behavior
|
||||
return existingValue;
|
||||
#endif
|
||||
|
||||
throw new JsonSerializationException($"Unexpected token type '{reader.TokenType}' when deserializing UnityEngine.Object");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1eb3a812e461dfa499f91f9500604ef3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/cn.tuanjie.codely.bridge/Editor/Tools.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0bd257c0ef5f4448dc88dfe19562c4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8301b99c61e7b7e44befcf784c72a226
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Registry for all Unity Tool command handlers (Upgraded Version)
|
||||
/// </summary>
|
||||
public static class CommandRegistry
|
||||
{
|
||||
// Maps command names to the corresponding static HandleCommand method in tool classes
|
||||
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
|
||||
{
|
||||
// Core tools
|
||||
{ "HandleManageScript", ManageScript.HandleCommand },
|
||||
{ "HandleManageScene", ManageScene.HandleCommand },
|
||||
{ "HandleManageEditor", ManageEditor.HandleCommand },
|
||||
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
|
||||
{ "HandleManageAsset", ManageAsset.HandleCommand },
|
||||
{ "HandleReadConsole", ReadConsole.HandleCommand },
|
||||
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand },
|
||||
{ "HandleManageShader", ManageShader.HandleCommand},
|
||||
{ "HandleManageScreenshot", ManageScreenshot.HandleCommand },
|
||||
// [EXPERIMENTAL] Phase 3 tools
|
||||
{ "HandleManagePackage", ManagePackage.HandleCommand },
|
||||
{ "HandleManageBake", ManageBake.HandleCommand },
|
||||
{ "HandleManageUIToolkit", ManageUIToolkit.HandleCommand },
|
||||
// Custom tool execution (API Spec aligned)
|
||||
{ "HandleExecuteCustomTool", ExecuteCustomTool.HandleCommand },
|
||||
{ "HandleExecuteCSharpScript", ExecuteCSharpScript.HandleCommand },
|
||||
// [INTERNAL] Not exposed to LLM - for agent execution layer only
|
||||
{ "Handle_InternalStateDirty", _InternalStateDirtyNotifier.HandleCommand },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets a command handler by name.
|
||||
/// </summary>
|
||||
/// <param name="commandName">Name of the command handler (e.g., "HandleManageAsset").</param>
|
||||
/// <returns>The command handler function if found, null otherwise.</returns>
|
||||
public static Func<JObject, object> GetHandler(string commandName)
|
||||
{
|
||||
// Use case-insensitive comparison for flexibility, although Python side should be consistent
|
||||
return _handlers.TryGetValue(commandName, out var handler) ? handler : null;
|
||||
// Consider adding logging here if a handler is not found
|
||||
/*
|
||||
if (_handlers.TryGetValue(commandName, out var handler)) {
|
||||
return handler;
|
||||
} else {
|
||||
UnityEngine.Debug.LogError($\"[CommandRegistry] No handler found for command: {commandName}\");
|
||||
return null;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 333f972789d338c4aafe2236cb5ac3cf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes C# scripts using Microsoft.CodeAnalysis.CSharp.Scripting (Roslyn).
|
||||
/// Captures and returns logs generated during script execution.
|
||||
/// Ensures execution happens on the main thread.
|
||||
/// </summary>
|
||||
public static class ExecuteCSharpScript
|
||||
{
|
||||
private static List<string> _capturedLogs = new List<string>();
|
||||
private static bool _isCapturingLogs = false;
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for executing C# scripts.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string script = @params["script"]?.ToString();
|
||||
if (string.IsNullOrEmpty(script))
|
||||
{
|
||||
return Response.Error("'script' parameter is required.");
|
||||
}
|
||||
|
||||
bool captureLogs = @params["capture_logs"]?.ToObject<bool>() ?? true;
|
||||
string[] imports = @params["imports"]?.ToObject<string[]>() ?? new string[]
|
||||
{
|
||||
"System",
|
||||
"System.Linq",
|
||||
"System.Collections.Generic",
|
||||
"UnityEngine",
|
||||
"UnityEditor",
|
||||
"UnityEditor.SceneManagement",
|
||||
"UnityEngine.SceneManagement"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"[ExecuteCSharpScript] Executing C# script (length: {script.Length} chars)");
|
||||
|
||||
StartLogCapture(captureLogs);
|
||||
|
||||
object result;
|
||||
try
|
||||
{
|
||||
result = ExecuteScriptInternal(script, imports);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Always stop log capture, even on error
|
||||
}
|
||||
|
||||
var logs = captureLogs ? StopLogCapture() : new List<string>();
|
||||
|
||||
return Response.Success(
|
||||
"C# script executed successfully.",
|
||||
new
|
||||
{
|
||||
result = result?.ToString(),
|
||||
logs = logs,
|
||||
log_count = logs.Count
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var logs = captureLogs ? StopLogCapture() : new List<string>();
|
||||
Debug.LogError($"[ExecuteCSharpScript] Failed to execute script: {e}");
|
||||
return Response.Error(
|
||||
$"C# script execution failed: {e.Message}",
|
||||
new
|
||||
{
|
||||
logs = logs,
|
||||
exception = e.ToString()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to execute the script using Roslyn.
|
||||
/// </summary>
|
||||
private static object ExecuteScriptInternal(string script, string[] imports)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Collect assembly references
|
||||
var references = new List<System.Reflection.Assembly>
|
||||
{
|
||||
typeof(UnityEngine.Debug).Assembly,
|
||||
typeof(UnityEditor.EditorApplication).Assembly
|
||||
};
|
||||
|
||||
// Add Assembly-CSharp if it exists (runtime user code)
|
||||
var assemblyCSharp = System.AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp");
|
||||
if (assemblyCSharp != null)
|
||||
{
|
||||
references.Add(assemblyCSharp);
|
||||
}
|
||||
|
||||
// Add Assembly-CSharp-Editor if it exists (editor user code)
|
||||
var assemblyCSharpEditor = System.AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp-Editor");
|
||||
if (assemblyCSharpEditor != null)
|
||||
{
|
||||
references.Add(assemblyCSharpEditor);
|
||||
}
|
||||
|
||||
// Add Unity.InputSystem if it exists (Input System package)
|
||||
var inputSystemAssembly = System.AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == "Unity.InputSystem");
|
||||
if (inputSystemAssembly != null)
|
||||
{
|
||||
references.Add(inputSystemAssembly);
|
||||
}
|
||||
|
||||
// Create script options with imports
|
||||
var options = ScriptOptions.Default
|
||||
.WithReferences(references)
|
||||
.WithImports(imports);
|
||||
|
||||
// Execute the script synchronously
|
||||
var scriptTask = CSharpScript.EvaluateAsync(script, options);
|
||||
|
||||
// Wait for the task to complete
|
||||
scriptTask.Wait();
|
||||
|
||||
return scriptTask.Result;
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
// Unwrap AggregateException to get the actual exception
|
||||
if (ae.InnerException != null)
|
||||
{
|
||||
throw ae.InnerException;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts capturing Unity logs.
|
||||
/// </summary>
|
||||
private static void StartLogCapture(bool enabled)
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
_isCapturingLogs = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_capturedLogs.Clear();
|
||||
_isCapturingLogs = true;
|
||||
Application.logMessageReceived += OnLogMessageReceived;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops capturing logs and returns the captured log list.
|
||||
/// </summary>
|
||||
private static List<string> StopLogCapture()
|
||||
{
|
||||
Application.logMessageReceived -= OnLogMessageReceived;
|
||||
_isCapturingLogs = false;
|
||||
|
||||
var logs = new List<string>(_capturedLogs);
|
||||
_capturedLogs.Clear();
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log message callback handler.
|
||||
/// </summary>
|
||||
private static void OnLogMessageReceived(string logString, string stackTrace, LogType type)
|
||||
{
|
||||
if (!_isCapturingLogs)
|
||||
return;
|
||||
|
||||
var logEntry = new StringBuilder();
|
||||
logEntry.Append($"[{type}] {logString}");
|
||||
|
||||
// Include stack trace for errors and exceptions
|
||||
if ((type == LogType.Error || type == LogType.Exception) && !string.IsNullOrEmpty(stackTrace))
|
||||
{
|
||||
logEntry.Append($"\n{stackTrace}");
|
||||
}
|
||||
|
||||
_capturedLogs.Add(logEntry.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a919bae4d47922248a206faa7ba67ed7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes custom tools registered in the Unity project.
|
||||
/// Custom tools must be static methods with a specific signature:
|
||||
/// public static object ToolName(JObject parameters)
|
||||
///
|
||||
/// Tools can be registered via the [CustomTool] attribute or
|
||||
/// by convention in the CustomToolsRegistry static class.
|
||||
/// </summary>
|
||||
public static class ExecuteCustomTool
|
||||
{
|
||||
// Registry of custom tools: tool_name -> method info
|
||||
private static readonly Dictionary<string, MethodInfo> _registeredTools = new Dictionary<string, MethodInfo>();
|
||||
private static bool _initialized = false;
|
||||
private static readonly object _initLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to mark a method as a custom tool.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class CustomToolAttribute : Attribute
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public CustomToolAttribute(string name, string description = null)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for executing custom tools.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
// Ensure tools are discovered
|
||||
EnsureInitialized();
|
||||
|
||||
string toolName = @params["tool_name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(toolName))
|
||||
{
|
||||
return Response.Error("'tool_name' parameter is required.");
|
||||
}
|
||||
|
||||
JObject toolParams = @params["parameters"] as JObject ?? new JObject();
|
||||
|
||||
try
|
||||
{
|
||||
// Check if tool exists
|
||||
if (!_registeredTools.TryGetValue(toolName, out MethodInfo method))
|
||||
{
|
||||
// Try case-insensitive lookup
|
||||
method = FindToolCaseInsensitive(toolName);
|
||||
if (method == null)
|
||||
{
|
||||
return Response.Error($"Custom tool '{toolName}' not found. Available tools: {string.Join(", ", _registeredTools.Keys)}");
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
Debug.Log($"[ExecuteCustomTool] Executing custom tool: {toolName}");
|
||||
var result = method.Invoke(null, new object[] { toolParams });
|
||||
|
||||
// Wrap result in standard response format if not already
|
||||
if (result is Dictionary<string, object> dictResult && dictResult.ContainsKey("success"))
|
||||
{
|
||||
// Already in standard format
|
||||
return result;
|
||||
}
|
||||
|
||||
// Wrap in success response
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Custom tool '{toolName}' executed successfully.",
|
||||
data = result,
|
||||
state = StateComposer.BuildFullState()
|
||||
};
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
// Unwrap the inner exception
|
||||
var innerEx = tie.InnerException ?? tie;
|
||||
Debug.LogError($"[ExecuteCustomTool] Tool '{toolName}' failed: {innerEx}");
|
||||
return Response.Error($"Custom tool '{toolName}' execution failed: {innerEx.Message}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ExecuteCustomTool] Error executing tool '{toolName}': {e}");
|
||||
return Response.Error($"Error executing custom tool '{toolName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom tool manually.
|
||||
/// </summary>
|
||||
public static void RegisterTool(string name, MethodInfo method)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_registeredTools.ContainsKey(name))
|
||||
{
|
||||
Debug.LogWarning($"[ExecuteCustomTool] Tool '{name}' is already registered. Overwriting.");
|
||||
}
|
||||
_registeredTools[name] = method;
|
||||
Debug.Log($"[ExecuteCustomTool] Registered custom tool: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all registered custom tools.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetRegisteredTools()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _registeredTools.Keys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and registers all custom tools in the project.
|
||||
/// </summary>
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Scan all assemblies for methods with [CustomTool] attribute
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
// Skip system assemblies for performance, but keep UnityTcp assemblies
|
||||
var assemblyName = assembly.GetName().Name;
|
||||
if (assemblyName.StartsWith("UnityTcp"))
|
||||
{
|
||||
// Always scan our own assemblies
|
||||
}
|
||||
else if (assemblyName.StartsWith("System") ||
|
||||
assemblyName.StartsWith("mscorlib") ||
|
||||
assemblyName.StartsWith("Unity") ||
|
||||
assemblyName.StartsWith("Newtonsoft") ||
|
||||
assemblyName.StartsWith("netstandard") ||
|
||||
assemblyName.StartsWith("Microsoft"))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
var attr = method.GetCustomAttribute<CustomToolAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
// Validate signature
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(JObject))
|
||||
{
|
||||
_registeredTools[attr.Name] = method;
|
||||
Debug.Log($"[ExecuteCustomTool] Discovered custom tool: {attr.Name} ({type.FullName}.{method.Name})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[ExecuteCustomTool] Invalid signature for tool '{attr.Name}'. Expected: public static object ToolName(JObject parameters)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ReflectionTypeLoadException)
|
||||
{
|
||||
// Ignore assembly load errors
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[ExecuteCustomTool] Initialization complete. {_registeredTools.Count} custom tools registered.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ExecuteCustomTool] Failed to initialize: {e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a tool by name (case-insensitive).
|
||||
/// </summary>
|
||||
private static MethodInfo FindToolCaseInsensitive(string toolName)
|
||||
{
|
||||
foreach (var kvp in _registeredTools)
|
||||
{
|
||||
if (string.Equals(kvp.Key, toolName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return kvp.Value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ef1416a06c8bce4caaff3a2b88aaafe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic; // Added for HashSet
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers; // For Response class
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles executing Unity Editor menu items by path.
|
||||
/// </summary>
|
||||
public static class ExecuteMenuItem
|
||||
{
|
||||
// Basic blacklist to prevent accidental execution of potentially disruptive menu items.
|
||||
// This can be expanded based on needs.
|
||||
private static readonly HashSet<string> _menuPathBlacklist = new HashSet<string>(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
)
|
||||
{
|
||||
"File/Quit",
|
||||
// Add other potentially dangerous items like "Edit/Preferences...", "File/Build Settings..." if needed
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for executing menu items or getting available ones.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = (@params["action"]?.ToString())?.ToLowerInvariant() ?? "execute"; // Default action
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "execute":
|
||||
return ExecuteItem(@params);
|
||||
case "get_available_menus":
|
||||
// Getting a comprehensive list of *all* menu items dynamically is very difficult
|
||||
// and often requires complex reflection or maintaining a manual list.
|
||||
// Returning a placeholder/acknowledgement for now.
|
||||
Debug.LogWarning(
|
||||
"[ExecuteMenuItem] 'get_available_menus' action is not fully implemented. Dynamically listing all menu items is complex."
|
||||
);
|
||||
// Returning an empty list as per the refactor plan's requirements.
|
||||
return Response.Success(
|
||||
"'get_available_menus' action is not fully implemented. Returning empty list.",
|
||||
new List<string>()
|
||||
);
|
||||
// TODO: Consider implementing a basic list of common/known menu items or exploring reflection techniques if this feature becomes critical.
|
||||
default:
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions are 'execute', 'get_available_menus'."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ExecuteMenuItem] Action '{action}' failed: {e}");
|
||||
return Response.Error($"Internal error processing action '{action}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a specific menu item.
|
||||
/// </summary>
|
||||
private static object ExecuteItem(JObject @params)
|
||||
{
|
||||
// Try both naming conventions: snake_case and camelCase
|
||||
string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
|
||||
// Optional future param retained for API compatibility; not used in synchronous mode
|
||||
// int timeoutMs = Math.Max(0, (@params["timeout_ms"]?.ToObject<int>() ?? 2000));
|
||||
|
||||
// string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
|
||||
// JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
|
||||
|
||||
if (string.IsNullOrWhiteSpace(menuPath))
|
||||
{
|
||||
return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
|
||||
}
|
||||
|
||||
// Validate against blacklist
|
||||
if (_menuPathBlacklist.Contains(menuPath))
|
||||
{
|
||||
return Response.Error(
|
||||
$"Execution of menu item '{menuPath}' is blocked for safety reasons."
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Implement alias lookup here if needed (Map alias to actual menuPath).
|
||||
// if (!string.IsNullOrEmpty(alias)) { menuPath = LookupAlias(alias); if(menuPath == null) return Response.Error(...); }
|
||||
|
||||
// TODO: Handle parameters ('parameters' object) if a viable method is found.
|
||||
// This is complex as EditorApplication.ExecuteMenuItem doesn't take arguments directly.
|
||||
// It might require finding the underlying EditorWindow or command if parameters are needed.
|
||||
|
||||
try
|
||||
{
|
||||
// Trace incoming execute requests (debug-gated)
|
||||
TcpLog.Info($"[ExecuteMenuItem] Request to execute menu: '{menuPath}'", always: false);
|
||||
|
||||
// Execute synchronously. This code runs on the Editor main thread in our bridge path.
|
||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||
if (executed)
|
||||
{
|
||||
// Success trace (debug-gated)
|
||||
TcpLog.Info($"[ExecuteMenuItem] Executed successfully: '{menuPath}'", always: false);
|
||||
return Response.Success(
|
||||
$"Executed menu item: '{menuPath}'",
|
||||
new { executed = true, menuPath }
|
||||
);
|
||||
}
|
||||
Debug.LogWarning($"[ExecuteMenuItem] Failed (not found/disabled): '{menuPath}'");
|
||||
return Response.Error(
|
||||
$"Failed to execute menu item (not found or disabled): '{menuPath}'",
|
||||
new { executed = false, menuPath }
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ExecuteMenuItem] Error executing '{menuPath}': {e}");
|
||||
return Response.Error($"Error executing menu item '{menuPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add helper for alias lookup if implementing aliases.
|
||||
// private static string LookupAlias(string alias) { ... return actualMenuPath or null ... }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 896e8045986eb0d449ee68395479f1d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2537
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageAsset.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d58d03981d97594b80e043a0ba78f55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
740
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageBake.cs
Normal file
@@ -0,0 +1,740 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// [EXPERIMENTAL] Handles baking operations (NavMesh, Lighting, etc.).
|
||||
/// Compatible with Unity 2022.3 LTS.
|
||||
/// </summary>
|
||||
public static class ManageBake
|
||||
{
|
||||
// Store callbacks for proper unsubscription
|
||||
private static readonly Dictionary<string, EditorApplication.CallbackFunction> _updateCallbacks = new Dictionary<string, EditorApplication.CallbackFunction>();
|
||||
private static readonly object _callbackLock = new object();
|
||||
// Store async operations for NavMesh baking
|
||||
private static readonly Dictionary<string, List<AsyncOperation>> _navMeshBakeOperations = new Dictionary<string, List<AsyncOperation>>();
|
||||
|
||||
// Runtime check for AI Navigation package availability
|
||||
private static bool? _hasAINavigation = null;
|
||||
private static Type _navMeshSurfaceType = null;
|
||||
private static MethodInfo _buildNavMeshMethod = null;
|
||||
private static MethodInfo _updateNavMeshMethod = null;
|
||||
private static PropertyInfo _activeSurfacesProperty = null;
|
||||
private static Type _navMeshType = null;
|
||||
private static MethodInfo _calculateTriangulationMethod = null;
|
||||
private static MethodInfo _removeAllNavMeshDataMethod = null;
|
||||
|
||||
/// <summary>
|
||||
/// Reset the AI Navigation package cache. Call this after installing the package
|
||||
/// to force re-checking for available types.
|
||||
/// </summary>
|
||||
private static void ResetAINavigationCache()
|
||||
{
|
||||
_hasAINavigation = null;
|
||||
_navMeshSurfaceType = null;
|
||||
_buildNavMeshMethod = null;
|
||||
_updateNavMeshMethod = null;
|
||||
_activeSurfacesProperty = null;
|
||||
_navMeshType = null;
|
||||
_calculateTriangulationMethod = null;
|
||||
_removeAllNavMeshDataMethod = null;
|
||||
}
|
||||
|
||||
private static bool HasAINavigation()
|
||||
{
|
||||
if (_hasAINavigation.HasValue)
|
||||
return _hasAINavigation.Value;
|
||||
|
||||
try
|
||||
{
|
||||
// First, check if the package is installed via PackageManager
|
||||
bool packageInstalled = false;
|
||||
try
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
// Use GetAllRegisteredPackages for Unity 2021.2+
|
||||
var packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
|
||||
packageInstalled = packages.Any(p => p.name == "com.unity.ai.navigation");
|
||||
#else
|
||||
// Fallback for older Unity versions
|
||||
var listRequest = Client.List(true, false);
|
||||
while (!listRequest.IsCompleted)
|
||||
{
|
||||
System.Threading.Thread.Sleep(50);
|
||||
}
|
||||
if (listRequest.Status == StatusCode.Success)
|
||||
{
|
||||
packageInstalled = listRequest.Result.Any(p => p.name == "com.unity.ai.navigation");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ManageBake] Error checking package installation: {ex.Message}");
|
||||
// Continue with type checking as fallback
|
||||
}
|
||||
|
||||
// Try to find NavMeshSurface type (Unity.AI.Navigation namespace from com.unity.ai.navigation package)
|
||||
// Try multiple methods to find the type
|
||||
_navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation");
|
||||
|
||||
if (_navMeshSurfaceType == null)
|
||||
{
|
||||
// Try with full assembly qualified name variations
|
||||
_navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
|
||||
}
|
||||
|
||||
if (_navMeshSurfaceType == null)
|
||||
{
|
||||
// Fallback: search in loaded assemblies by name first
|
||||
System.Reflection.Assembly targetAssembly = null;
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var assemblyName = assembly.GetName().Name;
|
||||
if (assemblyName == "Unity.AI.Navigation" || assemblyName.Contains("Unity.AI.Navigation"))
|
||||
{
|
||||
targetAssembly = assembly;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetAssembly != null)
|
||||
{
|
||||
_navMeshSurfaceType = targetAssembly.GetType("Unity.AI.Navigation.NavMeshSurface");
|
||||
}
|
||||
}
|
||||
|
||||
if (_navMeshSurfaceType == null)
|
||||
{
|
||||
// Last resort: search all assemblies
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
_navMeshSurfaceType = assembly.GetType("Unity.AI.Navigation.NavMeshSurface");
|
||||
if (_navMeshSurfaceType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_navMeshSurfaceType != null)
|
||||
{
|
||||
_buildNavMeshMethod = _navMeshSurfaceType.GetMethod("BuildNavMesh", BindingFlags.Public | BindingFlags.Instance);
|
||||
_updateNavMeshMethod = _navMeshSurfaceType.GetMethod("UpdateNavMesh", BindingFlags.Public | BindingFlags.Instance);
|
||||
_activeSurfacesProperty = _navMeshSurfaceType.GetProperty("activeSurfaces", BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
|
||||
// Try to find NavMesh type (UnityEngine.AI namespace - still used by the package)
|
||||
_navMeshType = Type.GetType("UnityEngine.AI.NavMesh, UnityEngine.AIModule");
|
||||
if (_navMeshType == null)
|
||||
{
|
||||
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
_navMeshType = assembly.GetType("UnityEngine.AI.NavMesh");
|
||||
if (_navMeshType != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_navMeshType != null)
|
||||
{
|
||||
_calculateTriangulationMethod = _navMeshType.GetMethod("CalculateTriangulation", BindingFlags.Public | BindingFlags.Static);
|
||||
_removeAllNavMeshDataMethod = _navMeshType.GetMethod("RemoveAllNavMeshData", BindingFlags.Public | BindingFlags.Static);
|
||||
}
|
||||
|
||||
// Check both package installation and required types/methods
|
||||
bool hasRequiredTypes = _navMeshSurfaceType != null && _buildNavMeshMethod != null && _navMeshType != null;
|
||||
|
||||
// If package is installed but types are missing, check compilation status
|
||||
if (packageInstalled && !hasRequiredTypes)
|
||||
{
|
||||
bool isCompiling = EditorApplication.isCompiling;
|
||||
string compilationStatus = isCompiling ? "compiling" : "idle";
|
||||
|
||||
// Collect diagnostic information
|
||||
var loadedAssemblies = System.AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a.GetName().Name.Contains("AI") || a.GetName().Name.Contains("Navigation"))
|
||||
.Select(a => a.GetName().Name)
|
||||
.ToList();
|
||||
|
||||
string diagnosticInfo = "";
|
||||
if (loadedAssemblies.Count > 0)
|
||||
{
|
||||
diagnosticInfo = $" Found related assemblies: {string.Join(", ", loadedAssemblies)}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
diagnosticInfo = " No AI/Navigation assemblies found in loaded assemblies.";
|
||||
}
|
||||
|
||||
string typeStatus = "";
|
||||
if (_navMeshSurfaceType == null)
|
||||
{
|
||||
typeStatus += " NavMeshSurface type not found.";
|
||||
}
|
||||
else
|
||||
{
|
||||
typeStatus += $" NavMeshSurface found, but methods missing: BuildNavMesh={_buildNavMeshMethod != null}, UpdateNavMesh={_updateNavMeshMethod != null}, activeSurfaces={_activeSurfacesProperty != null}.";
|
||||
}
|
||||
|
||||
if (_navMeshType == null)
|
||||
{
|
||||
typeStatus += " NavMesh type not found.";
|
||||
}
|
||||
|
||||
Debug.LogWarning(
|
||||
$"[ManageBake] com.unity.ai.navigation package is installed but required types/methods are not available. " +
|
||||
$"Editor is currently {compilationStatus}.{diagnosticInfo}{typeStatus} " +
|
||||
(isCompiling
|
||||
? "Please wait for compilation to complete, then call 'unity_editor { \"action\": \"wait_for_idle\" }' before retrying."
|
||||
: "The package may need to be reloaded. Try restarting Unity or wait a moment and retry.")
|
||||
);
|
||||
}
|
||||
|
||||
// Package installation check is primary, but we also need the types to be available
|
||||
// If package is installed but types are missing and we're not compiling, return false
|
||||
// If we're compiling, also return false (types won't be available until compilation completes)
|
||||
_hasAINavigation = packageInstalled && hasRequiredTypes && !EditorApplication.isCompiling;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ManageBake] Error checking for AI Navigation package: {ex.Message}");
|
||||
_hasAINavigation = false;
|
||||
}
|
||||
|
||||
return _hasAINavigation.Value;
|
||||
}
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return Response.Error("Action parameter is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "bake_navmesh":
|
||||
return BakeNavMesh(@params);
|
||||
case "bake_lighting":
|
||||
return BakeLighting(@params);
|
||||
case "wait_for_bake":
|
||||
return WaitForBake(@params);
|
||||
case "clear_navmesh":
|
||||
return ClearNavMesh();
|
||||
case "clear_baked_data":
|
||||
return ClearBakedData();
|
||||
default:
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions: bake_navmesh, bake_lighting, wait_for_bake, clear_navmesh, clear_baked_data."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ManageBake] Action '{action}' failed: {e}");
|
||||
return Response.Error($"[EXPERIMENTAL] Bake operation failed: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object BakeNavMesh(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Reset cache and re-check if first check fails (in case package was just installed)
|
||||
if (!HasAINavigation())
|
||||
{
|
||||
ResetAINavigationCache();
|
||||
if (!HasAINavigation())
|
||||
{
|
||||
// Check if package is installed but types are not available
|
||||
bool packageInstalled = false;
|
||||
try
|
||||
{
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
var packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages();
|
||||
packageInstalled = packages.Any(p => p.name == "com.unity.ai.navigation");
|
||||
#else
|
||||
var listRequest = Client.List(true, false);
|
||||
while (!listRequest.IsCompleted)
|
||||
{
|
||||
System.Threading.Thread.Sleep(50);
|
||||
}
|
||||
if (listRequest.Status == StatusCode.Success)
|
||||
{
|
||||
packageInstalled = listRequest.Result.Any(p => p.name == "com.unity.ai.navigation");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch { }
|
||||
|
||||
bool isCompiling = EditorApplication.isCompiling;
|
||||
|
||||
string errorMessage;
|
||||
if (packageInstalled && isCompiling)
|
||||
{
|
||||
errorMessage =
|
||||
"[EXPERIMENTAL] NavMesh baking requires AI Navigation package types to be loaded. " +
|
||||
"The package is installed but Unity is currently compiling. " +
|
||||
"Please wait for compilation to complete by calling 'unity_editor { \"action\": \"wait_for_idle\" }', then retry.";
|
||||
}
|
||||
else if (packageInstalled)
|
||||
{
|
||||
errorMessage =
|
||||
"[EXPERIMENTAL] NavMesh baking requires AI Navigation package types to be loaded. " +
|
||||
"The package 'com.unity.ai.navigation' is installed but required types are not available. " +
|
||||
"This may happen if: (1) compilation is in progress, (2) the package needs to be reloaded, or (3) Unity needs to be restarted. " +
|
||||
"Try: (1) Call 'unity_editor { \"action\": \"wait_for_idle\" }' to ensure compilation is complete, " +
|
||||
"(2) Wait a few seconds and retry, or (3) Restart Unity.";
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage =
|
||||
"[EXPERIMENTAL] NavMesh baking requires AI Navigation package. " +
|
||||
"Install 'com.unity.ai.navigation' via Package Manager using: " +
|
||||
"'unity_package { \"action\": \"install_package\", \"id_or_url\": \"com.unity.ai.navigation\" }', " +
|
||||
"then wait for installation and compilation to complete using 'unity_editor { \"action\": \"wait_for_idle\" }'.";
|
||||
}
|
||||
|
||||
return Response.Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed("bake_navmesh");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.NavMeshBake,
|
||||
"Baking NavMesh..."
|
||||
);
|
||||
|
||||
// Get all active NavMeshSurface components in the scene
|
||||
List<object> surfaces = new List<object>();
|
||||
if (_activeSurfacesProperty != null)
|
||||
{
|
||||
var activeSurfaces = _activeSurfacesProperty.GetValue(null);
|
||||
if (activeSurfaces is System.Collections.IList surfaceList)
|
||||
{
|
||||
foreach (var surface in surfaceList)
|
||||
{
|
||||
surfaces.Add(surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (surfaces.Count == 0)
|
||||
{
|
||||
// Fallback: find all NavMeshSurface components using Resources.FindObjectsOfTypeAll
|
||||
if (_navMeshSurfaceType != null)
|
||||
{
|
||||
var allObjects = Resources.FindObjectsOfTypeAll(_navMeshSurfaceType);
|
||||
foreach (var obj in allObjects)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
surfaces.Add(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (surfaces.Count == 0)
|
||||
{
|
||||
return Response.Error("[EXPERIMENTAL] No NavMeshSurface components found in the scene. Add a NavMeshSurface component to a GameObject to bake NavMesh.");
|
||||
}
|
||||
|
||||
// Check if we should use async baking (UpdateNavMesh) or sync baking (BuildNavMesh)
|
||||
bool useAsync = @params["async"]?.ToObject<bool?>() ?? false;
|
||||
List<AsyncOperation> asyncOps = new List<AsyncOperation>();
|
||||
|
||||
if (useAsync && _updateNavMeshMethod != null)
|
||||
{
|
||||
// Use async UpdateNavMesh for each surface that has existing data
|
||||
foreach (var surface in surfaces)
|
||||
{
|
||||
try
|
||||
{
|
||||
var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
var asyncOp = _updateNavMeshMethod.Invoke(surface, new object[] { navMeshData }) as AsyncOperation;
|
||||
if (asyncOp != null)
|
||||
{
|
||||
asyncOps.Add(asyncOp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If UpdateNavMesh fails, fall back to BuildNavMesh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no async operations were started, use synchronous BuildNavMesh
|
||||
if (asyncOps.Count == 0)
|
||||
{
|
||||
foreach (var surface in surfaces)
|
||||
{
|
||||
_buildNavMeshMethod?.Invoke(surface, null);
|
||||
}
|
||||
|
||||
// For synchronous baking, complete immediately
|
||||
bool hasNavMeshData = false;
|
||||
try
|
||||
{
|
||||
// First check NavMeshSurface components for navMeshData
|
||||
if (_navMeshSurfaceType != null)
|
||||
{
|
||||
var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null)
|
||||
{
|
||||
foreach (var surface in surfaces)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
hasNavMeshData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check global NavMesh if NavMeshSurface check didn't find anything
|
||||
if (!hasNavMeshData && _calculateTriangulationMethod != null)
|
||||
{
|
||||
var triangulation = _calculateTriangulationMethod.Invoke(null, null);
|
||||
if (triangulation != null)
|
||||
{
|
||||
var verticesProperty = triangulation.GetType().GetProperty("vertices");
|
||||
if (verticesProperty != null)
|
||||
{
|
||||
var vertices = verticesProperty.GetValue(triangulation) as Array;
|
||||
hasNavMeshData = vertices != null && vertices.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
AsyncOperationTracker.CompleteJob(job.OpId, "NavMesh baking completed", new
|
||||
{
|
||||
hasNavMeshData = hasNavMeshData,
|
||||
surfacesBaked = surfaces.Count
|
||||
});
|
||||
StateComposer.IncrementRevision();
|
||||
|
||||
return AsyncOperationTracker.CreateCompleteResponse(job);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store async operations for tracking
|
||||
lock (_callbackLock)
|
||||
{
|
||||
_navMeshBakeOperations[job.OpId] = asyncOps;
|
||||
}
|
||||
|
||||
// Create and store callback delegate for proper unsubscription
|
||||
EditorApplication.CallbackFunction callback = () => CheckNavMeshBake(job.OpId);
|
||||
lock (_callbackLock)
|
||||
{
|
||||
_updateCallbacks[job.OpId] = callback;
|
||||
}
|
||||
EditorApplication.update += callback;
|
||||
|
||||
// Return standardized pending response
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary<string, object>;
|
||||
response["poll_interval"] = 2.0;
|
||||
response["message"] = "[EXPERIMENTAL] NavMesh baking started (async)";
|
||||
response["data"] = new { type = "navmesh", surfacesCount = surfaces.Count };
|
||||
return response;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to start NavMesh baking: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckNavMeshBake(string opId)
|
||||
{
|
||||
if (!HasAINavigation())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if async operations are still running
|
||||
List<AsyncOperation> asyncOps = null;
|
||||
lock (_callbackLock)
|
||||
{
|
||||
if (_navMeshBakeOperations.TryGetValue(opId, out asyncOps))
|
||||
{
|
||||
// Check if all operations are done
|
||||
bool allDone = asyncOps.All(op => op != null && op.isDone);
|
||||
|
||||
if (allDone)
|
||||
{
|
||||
// Remove from tracking
|
||||
_navMeshBakeOperations.Remove(opId);
|
||||
|
||||
// Properly unsubscribe using stored delegate
|
||||
if (_updateCallbacks.TryGetValue(opId, out var callback))
|
||||
{
|
||||
EditorApplication.update -= callback;
|
||||
_updateCallbacks.Remove(opId);
|
||||
}
|
||||
|
||||
// Check if NavMesh data exists using reflection
|
||||
bool hasNavMeshData = false;
|
||||
try
|
||||
{
|
||||
// First check NavMeshSurface components for navMeshData
|
||||
if (_navMeshSurfaceType != null)
|
||||
{
|
||||
var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData");
|
||||
if (navMeshDataProperty != null && _activeSurfacesProperty != null)
|
||||
{
|
||||
var activeSurfaces = _activeSurfacesProperty.GetValue(null);
|
||||
if (activeSurfaces is System.Collections.IList surfaceList)
|
||||
{
|
||||
foreach (var surface in surfaceList)
|
||||
{
|
||||
if (surface != null)
|
||||
{
|
||||
var navMeshData = navMeshDataProperty.GetValue(surface);
|
||||
if (navMeshData != null)
|
||||
{
|
||||
hasNavMeshData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check global NavMesh if NavMeshSurface check didn't find anything
|
||||
if (!hasNavMeshData && _calculateTriangulationMethod != null)
|
||||
{
|
||||
var triangulation = _calculateTriangulationMethod.Invoke(null, null);
|
||||
if (triangulation != null)
|
||||
{
|
||||
var verticesProperty = triangulation.GetType().GetProperty("vertices");
|
||||
if (verticesProperty != null)
|
||||
{
|
||||
var vertices = verticesProperty.GetValue(triangulation) as Array;
|
||||
hasNavMeshData = vertices != null && vertices.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't check, assume no data
|
||||
hasNavMeshData = false;
|
||||
}
|
||||
|
||||
AsyncOperationTracker.CompleteJob(opId, "NavMesh baking completed", new
|
||||
{
|
||||
hasNavMeshData = hasNavMeshData,
|
||||
surfacesBaked = asyncOps.Count
|
||||
});
|
||||
|
||||
StateComposer.IncrementRevision();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ManageBake] Error in CheckNavMeshBake: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object BakeLighting(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed("bake_lighting");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.LightingBake,
|
||||
"Baking lighting..."
|
||||
);
|
||||
|
||||
// Start async bake
|
||||
Lightmapping.BakeAsync();
|
||||
|
||||
// Create and store callback delegate for proper unsubscription
|
||||
EditorApplication.CallbackFunction callback = () => CheckLightingBake(job.OpId);
|
||||
lock (_callbackLock)
|
||||
{
|
||||
_updateCallbacks[job.OpId] = callback;
|
||||
}
|
||||
EditorApplication.update += callback;
|
||||
|
||||
// Return standardized pending response
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary<string, object>;
|
||||
response["poll_interval"] = 2.0;
|
||||
response["message"] = "[EXPERIMENTAL] Lighting baking started";
|
||||
response["data"] = new { type = "lighting" };
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to start lighting baking: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckLightingBake(string opId)
|
||||
{
|
||||
if (!Lightmapping.isRunning)
|
||||
{
|
||||
// Properly unsubscribe using stored delegate
|
||||
EditorApplication.CallbackFunction callback;
|
||||
lock (_callbackLock)
|
||||
{
|
||||
if (_updateCallbacks.TryGetValue(opId, out callback))
|
||||
{
|
||||
EditorApplication.update -= callback;
|
||||
_updateCallbacks.Remove(opId);
|
||||
}
|
||||
}
|
||||
|
||||
AsyncOperationTracker.CompleteJob(opId, "Lighting baking completed", new
|
||||
{
|
||||
hasLightingData = Lightmapping.lightingDataAsset != null
|
||||
});
|
||||
|
||||
StateComposer.IncrementRevision();
|
||||
}
|
||||
}
|
||||
|
||||
private static object WaitForBake(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
string opId = @params["op_id"]?.ToString();
|
||||
int timeoutSeconds = @params["timeoutSeconds"]?.ToObject<int?>() ?? 600;
|
||||
|
||||
if (string.IsNullOrEmpty(opId))
|
||||
return Response.Error("'op_id' parameter required for wait_for_bake.");
|
||||
|
||||
var job = AsyncOperationTracker.GetJob(opId);
|
||||
if (job == null)
|
||||
return Response.Error($"Operation {opId} not found.");
|
||||
|
||||
if (job.Type != AsyncOperationTracker.JobType.NavMeshBake &&
|
||||
job.Type != AsyncOperationTracker.JobType.LightingBake)
|
||||
return Response.Error($"Operation {opId} is not a bake operation.");
|
||||
|
||||
if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds))
|
||||
{
|
||||
AsyncOperationTracker.FailJob(opId, $"Bake operation timed out after {timeoutSeconds} seconds");
|
||||
return AsyncOperationTracker.CreateErrorResponse(job);
|
||||
}
|
||||
|
||||
switch (job.Status)
|
||||
{
|
||||
case AsyncOperationTracker.JobStatus.Complete:
|
||||
var response = AsyncOperationTracker.CreateCompleteResponse(job);
|
||||
return response;
|
||||
case AsyncOperationTracker.JobStatus.Error:
|
||||
return AsyncOperationTracker.CreateErrorResponse(job);
|
||||
case AsyncOperationTracker.JobStatus.Pending:
|
||||
return AsyncOperationTracker.CreatePendingResponse(job);
|
||||
default:
|
||||
return Response.Error($"Unknown job status: {job.Status}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to wait for bake: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ClearNavMesh()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!HasAINavigation())
|
||||
{
|
||||
return Response.Error("[EXPERIMENTAL] NavMesh operations require AI Navigation package.");
|
||||
}
|
||||
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed("clear_navmesh");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
// Clear NavMesh using reflection - try RemoveAllNavMeshData first
|
||||
int clearedCount = 0;
|
||||
if (_removeAllNavMeshDataMethod != null)
|
||||
{
|
||||
_removeAllNavMeshDataMethod.Invoke(null, null);
|
||||
clearedCount++;
|
||||
}
|
||||
|
||||
// Also clear all NavMeshSurface components
|
||||
if (_activeSurfacesProperty != null)
|
||||
{
|
||||
var activeSurfaces = _activeSurfacesProperty.GetValue(null);
|
||||
if (activeSurfaces is System.Collections.IList surfaceList)
|
||||
{
|
||||
var removeDataMethod = _navMeshSurfaceType.GetMethod("RemoveData", BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var surface in surfaceList)
|
||||
{
|
||||
try
|
||||
{
|
||||
removeDataMethod?.Invoke(surface, null);
|
||||
clearedCount++;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateComposer.IncrementRevision();
|
||||
|
||||
return Response.Success($"[EXPERIMENTAL] NavMesh data cleared ({clearedCount} surfaces).");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to clear NavMesh: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ClearBakedData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed("clear_baked_data");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
Lightmapping.Clear();
|
||||
StateComposer.IncrementRevision();
|
||||
|
||||
return Response.Success("[EXPERIMENTAL] Baked lighting data cleared.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to clear baked data: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16bc91f90f3df674486759e40dffb088
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1330
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageEditor.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da88809c82aa8214eaa168ec9ce090af
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
4592
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageGameObject.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c6230a18f5554a41aaac2188776332e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
271
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManagePackage.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers;
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// [EXPERIMENTAL] Handles Unity Package Manager (UPM) operations.
|
||||
/// Supports installing, removing, and querying packages.
|
||||
/// Compatible with Unity 2022.3 LTS.
|
||||
/// </summary>
|
||||
public static class ManagePackage
|
||||
{
|
||||
private static readonly Dictionary<string, Request> _activeRequests = new Dictionary<string, Request>();
|
||||
private static readonly Dictionary<string, EditorApplication.CallbackFunction> _updateCallbacks = new Dictionary<string, EditorApplication.CallbackFunction>();
|
||||
private static readonly object _requestLock = new object();
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return Response.Error("Action parameter is required.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "install_package":
|
||||
return InstallPackage(@params);
|
||||
case "remove_package":
|
||||
return RemovePackage(@params);
|
||||
case "wait_for_upm":
|
||||
return WaitForUpm(@params);
|
||||
case "list_packages":
|
||||
return ListPackages();
|
||||
default:
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions: install_package, remove_package, wait_for_upm, list_packages."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ManagePackage] Action '{action}' failed: {e}");
|
||||
return Response.Error($"[EXPERIMENTAL] Package operation failed: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object InstallPackage(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
string idOrUrl = @params["id_or_url"]?.ToString();
|
||||
if (string.IsNullOrEmpty(idOrUrl))
|
||||
return Response.Error("'id_or_url' parameter required for install_package.");
|
||||
|
||||
// Handle optional version parameter
|
||||
string version = @params["version"]?.ToString();
|
||||
|
||||
// Append version to package identifier if provided and not already in format
|
||||
// e.g., "com.unity.package" + "1.2.3" -> "com.unity.package@1.2.3"
|
||||
if (!string.IsNullOrEmpty(version) &&
|
||||
!idOrUrl.Contains("@") &&
|
||||
!idOrUrl.StartsWith("http"))
|
||||
{
|
||||
idOrUrl = $"{idOrUrl}@{version}";
|
||||
}
|
||||
|
||||
// Create job
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.UpmPackage,
|
||||
$"Installing package: {idOrUrl}"
|
||||
);
|
||||
|
||||
// Start UPM request
|
||||
AddRequest addRequest = Client.Add(idOrUrl);
|
||||
lock (_requestLock)
|
||||
{
|
||||
_activeRequests[job.OpId] = addRequest;
|
||||
}
|
||||
|
||||
// Create and store callback delegate for proper unsubscription
|
||||
EditorApplication.CallbackFunction callback = () => CheckUpmRequest(job.OpId);
|
||||
lock (_requestLock)
|
||||
{
|
||||
_updateCallbacks[job.OpId] = callback;
|
||||
}
|
||||
EditorApplication.update += callback;
|
||||
|
||||
// Return standardized pending response
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary<string, object>;
|
||||
response["poll_interval"] = 2.0; // UPM needs longer poll interval
|
||||
response["message"] = $"[EXPERIMENTAL] Package installation started: {idOrUrl}";
|
||||
response["data"] = new { package = idOrUrl, type = "install" };
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to start package installation: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object RemovePackage(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
string packageName = @params["package_name"]?.ToString();
|
||||
if (string.IsNullOrEmpty(packageName))
|
||||
return Response.Error("'package_name' parameter required for remove_package.");
|
||||
|
||||
var job = AsyncOperationTracker.CreateJob(
|
||||
AsyncOperationTracker.JobType.UpmPackage,
|
||||
$"Removing package: {packageName}"
|
||||
);
|
||||
|
||||
RemoveRequest removeRequest = Client.Remove(packageName);
|
||||
lock (_requestLock)
|
||||
{
|
||||
_activeRequests[job.OpId] = removeRequest;
|
||||
}
|
||||
|
||||
// Create and store callback delegate for proper unsubscription
|
||||
EditorApplication.CallbackFunction callback = () => CheckUpmRequest(job.OpId);
|
||||
lock (_requestLock)
|
||||
{
|
||||
_updateCallbacks[job.OpId] = callback;
|
||||
}
|
||||
EditorApplication.update += callback;
|
||||
|
||||
// Return standardized pending response
|
||||
var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary<string, object>;
|
||||
response["poll_interval"] = 2.0; // UPM needs longer poll interval
|
||||
response["message"] = $"[EXPERIMENTAL] Package removal started: {packageName}";
|
||||
response["data"] = new { package = packageName, type = "remove" };
|
||||
return response;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to start package removal: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckUpmRequest(string opId)
|
||||
{
|
||||
Request request;
|
||||
EditorApplication.CallbackFunction callback;
|
||||
lock (_requestLock)
|
||||
{
|
||||
if (!_activeRequests.TryGetValue(opId, out request))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_updateCallbacks.TryGetValue(opId, out callback);
|
||||
}
|
||||
|
||||
if (request.IsCompleted)
|
||||
{
|
||||
// Properly unsubscribe using stored delegate
|
||||
if (callback != null)
|
||||
{
|
||||
EditorApplication.update -= callback;
|
||||
}
|
||||
|
||||
lock (_requestLock)
|
||||
{
|
||||
_activeRequests.Remove(opId);
|
||||
_updateCallbacks.Remove(opId);
|
||||
}
|
||||
|
||||
if (request.Status == StatusCode.Success)
|
||||
{
|
||||
AsyncOperationTracker.CompleteJob(opId, "Package operation completed successfully");
|
||||
StateComposer.IncrementRevision();
|
||||
}
|
||||
else
|
||||
{
|
||||
AsyncOperationTracker.FailJob(opId, $"Package operation failed: {request.Error?.message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static object WaitForUpm(JObject @params)
|
||||
{
|
||||
try
|
||||
{
|
||||
string opId = @params["op_id"]?.ToString();
|
||||
int timeoutSeconds = @params["timeoutSeconds"]?.ToObject<int?>() ?? 300;
|
||||
|
||||
if (string.IsNullOrEmpty(opId))
|
||||
return Response.Error("'op_id' parameter required for wait_for_upm.");
|
||||
|
||||
var job = AsyncOperationTracker.GetJob(opId);
|
||||
if (job == null)
|
||||
return Response.Error($"Operation {opId} not found.");
|
||||
|
||||
if (job.Type != AsyncOperationTracker.JobType.UpmPackage)
|
||||
return Response.Error($"Operation {opId} is not a UPM operation.");
|
||||
|
||||
if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds))
|
||||
{
|
||||
AsyncOperationTracker.FailJob(opId, $"UPM operation timed out after {timeoutSeconds} seconds");
|
||||
return AsyncOperationTracker.CreateErrorResponse(job);
|
||||
}
|
||||
|
||||
switch (job.Status)
|
||||
{
|
||||
case AsyncOperationTracker.JobStatus.Complete:
|
||||
return AsyncOperationTracker.CreateCompleteResponse(job);
|
||||
case AsyncOperationTracker.JobStatus.Error:
|
||||
return AsyncOperationTracker.CreateErrorResponse(job);
|
||||
case AsyncOperationTracker.JobStatus.Pending:
|
||||
return AsyncOperationTracker.CreatePendingResponse(job);
|
||||
default:
|
||||
return Response.Error($"Unknown job status: {job.Status}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to wait for UPM: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object ListPackages()
|
||||
{
|
||||
try
|
||||
{
|
||||
ListRequest listRequest = Client.List(true, false);
|
||||
|
||||
// Wait for request to complete (synchronous for simplicity)
|
||||
while (!listRequest.IsCompleted)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
if (listRequest.Status == StatusCode.Success)
|
||||
{
|
||||
var packages = listRequest.Result.Select(p => new
|
||||
{
|
||||
name = p.name,
|
||||
version = p.version,
|
||||
displayName = p.displayName,
|
||||
description = p.description,
|
||||
source = p.source.ToString()
|
||||
}).ToList();
|
||||
|
||||
return Response.Success(
|
||||
$"[EXPERIMENTAL] Retrieved {packages.Count} packages.",
|
||||
packages
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to list packages: {listRequest.Error?.message}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"[EXPERIMENTAL] Failed to list packages: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d06adc1d5b654a428a23760d94d5079
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
722
Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageScene.cs
Normal file
@@ -0,0 +1,722 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityTcp.Editor.Helpers; // For Response class
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
|
||||
/// </summary>
|
||||
public static class ManageScene
|
||||
{
|
||||
private static readonly string[] AllowedSceneExtensions = new[] { ".unity", ".scene" };
|
||||
|
||||
#if TUANJIE_1_OR_NEWER || TUANJIE_1
|
||||
// Tuanjie Editor defines TUANJIE_* version macros (e.g. TUANJIE_1, TUANJIE_1_1, TUANJIE_1_1_2, TUANJIE_X_Y_OR_NEWER).
|
||||
private const bool IsTuanjieEditor = true;
|
||||
#else
|
||||
// Unity Editor builds do NOT define TUANJIE_* macros.
|
||||
private const bool IsTuanjieEditor = false;
|
||||
#endif
|
||||
|
||||
private static bool HasAllowedSceneExtension(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return false;
|
||||
return AllowedSceneExtensions.Any(ext =>
|
||||
path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
||||
private static string GetSceneExtensionFromPathOrName(string path, string name)
|
||||
{
|
||||
// If caller provided an explicit scene file path, respect its extension.
|
||||
if (!string.IsNullOrEmpty(path) && HasAllowedSceneExtension(path))
|
||||
{
|
||||
return Path.GetExtension(path).ToLowerInvariant();
|
||||
}
|
||||
|
||||
// If caller (incorrectly) included an extension in name, respect it as a hint.
|
||||
if (!string.IsNullOrEmpty(name) && HasAllowedSceneExtension(name))
|
||||
{
|
||||
return Path.GetExtension(name).ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Default depends on editor:
|
||||
// - Unity: .unity
|
||||
// - Tuanjie: .scene
|
||||
return IsTuanjieEditor ? ".scene" : ".unity";
|
||||
}
|
||||
|
||||
private static string StripSceneExtension(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return name;
|
||||
foreach (var ext in AllowedSceneExtensions)
|
||||
{
|
||||
if (name.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return name.Substring(0, name.Length - ext.Length);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private sealed class SceneCommand
|
||||
{
|
||||
public string action { get; set; } = string.Empty;
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string path { get; set; } = string.Empty;
|
||||
public int? buildIndex { get; set; }
|
||||
}
|
||||
|
||||
private static SceneCommand ToSceneCommand(JObject p)
|
||||
{
|
||||
if (p == null) return new SceneCommand();
|
||||
int? BI(JToken t)
|
||||
{
|
||||
if (t == null || t.Type == JTokenType.Null) return null;
|
||||
var s = t.ToString().Trim();
|
||||
if (s.Length == 0) return null;
|
||||
if (int.TryParse(s, out var i)) return i;
|
||||
if (double.TryParse(s, out var d)) return (int)d;
|
||||
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
|
||||
}
|
||||
return new SceneCommand
|
||||
{
|
||||
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
|
||||
name = p["name"]?.ToString() ?? string.Empty,
|
||||
path = p["path"]?.ToString() ?? string.Empty,
|
||||
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for scene management actions.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
try { TcpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
|
||||
var cmd = ToSceneCommand(@params);
|
||||
string action = cmd.action;
|
||||
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
|
||||
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
|
||||
int? buildIndex = cmd.buildIndex;
|
||||
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
|
||||
|
||||
// --- Validate client_state_rev for write operations ---
|
||||
var writeActions = new[] { "ensure_scene_open", "ensure_scene_saved", "create", "load", "save" };
|
||||
if (Array.Exists(writeActions, a => a == action))
|
||||
{
|
||||
var revConflict = StateComposer.ValidateClientRevisionFromParams(@params);
|
||||
if (revConflict != null) return revConflict;
|
||||
}
|
||||
|
||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||
string relativeDir = path ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(relativeDir))
|
||||
{
|
||||
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
|
||||
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default *after* sanitizing, using the original path variable for the check
|
||||
if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness
|
||||
{
|
||||
relativeDir = "Scenes"; // Default relative directory
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return Response.Error("Action parameter is required.");
|
||||
}
|
||||
|
||||
// Normalize name (strip extension if provided)
|
||||
name = StripSceneExtension(name);
|
||||
|
||||
// Determine extension for scene file operations (Unity: .unity, Tuanjie: .scene)
|
||||
string sceneExt = GetSceneExtensionFromPathOrName(path, cmd.name);
|
||||
|
||||
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}{sceneExt}";
|
||||
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
|
||||
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
|
||||
string fullPath = string.IsNullOrEmpty(sceneFileName)
|
||||
? null
|
||||
: Path.Combine(fullPathDir, sceneFileName);
|
||||
// Ensure relativePath always starts with "Assets/" and uses forward slashes
|
||||
string relativePath = string.IsNullOrEmpty(sceneFileName)
|
||||
? null
|
||||
: Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/');
|
||||
|
||||
// Ensure directory exists for 'create'
|
||||
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(fullPathDir);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error(
|
||||
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Route action
|
||||
try { TcpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { }
|
||||
switch (action)
|
||||
{
|
||||
// Ensure operations (idempotent)
|
||||
case "ensure_scene_open":
|
||||
// For ensure_scene_open, the path parameter is the full scene path (e.g., "Assets/Scenes/MyScene.unity")
|
||||
// NOT the directory path like other actions
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return Response.Error("'path' parameter is required for 'ensure_scene_open' action.");
|
||||
// Normalize the path - ensure it starts with Assets/ and uses forward slashes
|
||||
string ensureScenePath = path.Replace('\\', '/');
|
||||
if (!ensureScenePath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
ensureScenePath = "Assets/" + ensureScenePath.TrimStart('/');
|
||||
if (!HasAllowedSceneExtension(ensureScenePath))
|
||||
return Response.Error("'path' must end with '.unity' or '.scene' for scene files.");
|
||||
return EnsureSceneOpen(ensureScenePath);
|
||||
case "ensure_scene_saved":
|
||||
return EnsureSceneSaved();
|
||||
|
||||
// Regular operations
|
||||
case "create":
|
||||
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
|
||||
return Response.Error(
|
||||
"'name' and 'path' parameters are required for 'create' action."
|
||||
);
|
||||
return CreateScene(fullPath, relativePath);
|
||||
case "load":
|
||||
// Loading can be done by path/name or build index
|
||||
if (!string.IsNullOrEmpty(relativePath))
|
||||
return LoadScene(relativePath);
|
||||
else if (buildIndex.HasValue)
|
||||
return LoadScene(buildIndex.Value);
|
||||
else
|
||||
return Response.Error(
|
||||
"Either 'name'/'path' or 'buildIndex' must be provided for 'load' action."
|
||||
);
|
||||
case "save":
|
||||
// Save current scene, optionally to a new path
|
||||
return SaveScene(fullPath, relativePath);
|
||||
case "get_hierarchy":
|
||||
try { TcpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
|
||||
var gh = GetSceneHierarchy();
|
||||
try { TcpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
|
||||
return gh;
|
||||
case "get_active":
|
||||
try { TcpLog.Info("[ManageScene] get_active: entering", always: false); } catch { }
|
||||
var ga = GetActiveSceneInfo();
|
||||
try { TcpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { }
|
||||
return ga;
|
||||
case "get_build_settings":
|
||||
return GetBuildSettingsScenes();
|
||||
// Add cases for modifying build settings, additive loading, unloading etc.
|
||||
default:
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions: ensure_scene_open, ensure_scene_saved, create, load, save, get_hierarchy, get_active, get_build_settings."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static object CreateScene(string fullPath, string relativePath)
|
||||
{
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
return Response.Error($"Scene already exists at '{relativePath}'.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new empty scene
|
||||
Scene newScene = EditorSceneManager.NewScene(
|
||||
NewSceneSetup.EmptyScene,
|
||||
NewSceneMode.Single
|
||||
);
|
||||
// Save it to the specified path (creation is an authoring operation)
|
||||
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
|
||||
|
||||
if (saved)
|
||||
{
|
||||
AssetDatabase.Refresh(); // Ensure Unity sees the new scene file
|
||||
return Response.Success(
|
||||
$"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.",
|
||||
new { path = relativePath }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If SaveScene fails, it might leave an untitled scene open.
|
||||
// Optionally try to close it, but be cautious.
|
||||
return Response.Error($"Failed to save new scene to '{relativePath}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Error creating scene '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object LoadScene(string relativePath)
|
||||
{
|
||||
if (
|
||||
!File.Exists(
|
||||
Path.Combine(
|
||||
Application.dataPath.Substring(
|
||||
0,
|
||||
Application.dataPath.Length - "Assets".Length
|
||||
),
|
||||
relativePath
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
return Response.Error($"Scene file not found at '{relativePath}'.");
|
||||
}
|
||||
|
||||
// Check for unsaved changes in the current scene
|
||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||
{
|
||||
// Optionally prompt the user or save automatically before loading
|
||||
return Response.Error(
|
||||
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||
);
|
||||
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
|
||||
// if (!saveOK) return Response.Error("Load cancelled by user.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
|
||||
return Response.Success(
|
||||
$"Scene '{relativePath}' loaded successfully.",
|
||||
new
|
||||
{
|
||||
path = relativePath,
|
||||
name = Path.GetFileNameWithoutExtension(relativePath),
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Error loading scene '{relativePath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object LoadScene(int buildIndex)
|
||||
{
|
||||
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
|
||||
{
|
||||
return Response.Error(
|
||||
$"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}."
|
||||
);
|
||||
}
|
||||
|
||||
// Check for unsaved changes
|
||||
if (EditorSceneManager.GetActiveScene().isDirty)
|
||||
{
|
||||
return Response.Error(
|
||||
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
|
||||
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
return Response.Success(
|
||||
$"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.",
|
||||
new
|
||||
{
|
||||
path = scenePath,
|
||||
name = Path.GetFileNameWithoutExtension(scenePath),
|
||||
buildIndex = buildIndex,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error(
|
||||
$"Error loading scene with build index {buildIndex}: {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static object SaveScene(string fullPath, string relativePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
Scene currentScene = EditorSceneManager.GetActiveScene();
|
||||
if (!currentScene.IsValid())
|
||||
{
|
||||
return Response.Error("No valid scene is currently active to save.");
|
||||
}
|
||||
|
||||
bool saved;
|
||||
string finalPath = currentScene.path; // Path where it was last saved or will be saved
|
||||
|
||||
if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath)
|
||||
{
|
||||
// Save As...
|
||||
// Ensure directory exists
|
||||
string dir = Path.GetDirectoryName(fullPath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
|
||||
finalPath = relativePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Save (overwrite existing or save untitled)
|
||||
if (string.IsNullOrEmpty(currentScene.path))
|
||||
{
|
||||
// Scene is untitled, needs a path
|
||||
return Response.Error(
|
||||
"Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality."
|
||||
);
|
||||
}
|
||||
saved = EditorSceneManager.SaveScene(currentScene);
|
||||
}
|
||||
|
||||
if (saved)
|
||||
{
|
||||
AssetDatabase.Refresh();
|
||||
return Response.Success(
|
||||
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
|
||||
new { path = finalPath, name = currentScene.name }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error($"Failed to save scene '{currentScene.name}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Error saving scene: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetActiveSceneInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
try { TcpLog.Info("[ManageScene] get_active: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
|
||||
Scene activeScene = EditorSceneManager.GetActiveScene();
|
||||
try { TcpLog.Info($"[ManageScene] get_active: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
|
||||
if (!activeScene.IsValid())
|
||||
{
|
||||
return Response.Error("No active scene found.");
|
||||
}
|
||||
|
||||
var sceneInfo = new
|
||||
{
|
||||
name = activeScene.name,
|
||||
path = activeScene.path,
|
||||
buildIndex = activeScene.buildIndex, // -1 if not in build settings
|
||||
isDirty = activeScene.isDirty,
|
||||
isLoaded = activeScene.isLoaded,
|
||||
rootCount = activeScene.rootCount,
|
||||
};
|
||||
|
||||
return Response.Success("Retrieved active scene information.", sceneInfo);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { TcpLog.Error($"[ManageScene] get_active: exception {e.Message}"); } catch { }
|
||||
return Response.Error($"Error getting active scene info: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetBuildSettingsScenes()
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenes = new List<object>();
|
||||
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
|
||||
{
|
||||
var scene = EditorBuildSettings.scenes[i];
|
||||
scenes.Add(
|
||||
new
|
||||
{
|
||||
path = scene.path,
|
||||
guid = scene.guid.ToString(),
|
||||
enabled = scene.enabled,
|
||||
buildIndex = i, // Actual build index considering only enabled scenes might differ
|
||||
}
|
||||
);
|
||||
}
|
||||
return Response.Success("Retrieved scenes from Build Settings.", scenes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Error getting scenes from Build Settings: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object GetSceneHierarchy()
|
||||
{
|
||||
try
|
||||
{
|
||||
try { TcpLog.Info("[ManageScene] get_hierarchy: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
|
||||
Scene activeScene = EditorSceneManager.GetActiveScene();
|
||||
try { TcpLog.Info($"[ManageScene] get_hierarchy: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
|
||||
if (!activeScene.IsValid() || !activeScene.isLoaded)
|
||||
{
|
||||
return Response.Error(
|
||||
"No valid and loaded scene is active to get hierarchy from."
|
||||
);
|
||||
}
|
||||
|
||||
try { TcpLog.Info("[ManageScene] get_hierarchy: fetching root objects", always: false); } catch { }
|
||||
GameObject[] rootObjects = activeScene.GetRootGameObjects();
|
||||
try { TcpLog.Info($"[ManageScene] get_hierarchy: rootCount={rootObjects?.Length ?? 0}", always: false); } catch { }
|
||||
|
||||
// Count total GameObjects to avoid massive responses
|
||||
int totalObjectCount = 0;
|
||||
foreach (var rootObj in rootObjects)
|
||||
{
|
||||
totalObjectCount += CountGameObjectsRecursive(rootObj);
|
||||
}
|
||||
|
||||
// If too many objects would be serialized, return a shallow root-only tree with hints.
|
||||
if (totalObjectCount > 500)
|
||||
{
|
||||
var roots = rootObjects
|
||||
.Where(go => go != null)
|
||||
.Select(go => new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
{ "instanceID", go.GetInstanceID() },
|
||||
{ "activeSelf", go.activeSelf },
|
||||
{ "activeInHierarchy", go.activeInHierarchy },
|
||||
{ "tag", go.tag },
|
||||
{ "layer", go.layer },
|
||||
{ "isStatic", go.isStatic },
|
||||
{ "childCount", go.transform != null ? go.transform.childCount : 0 },
|
||||
{ "children", new List<object>() }, // Keep tree shape (shallow)
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Response.Success(
|
||||
$"Scene hierarchy is large ({totalObjectCount} GameObjects). Returned root objects only (children omitted).",
|
||||
new
|
||||
{
|
||||
partial = true,
|
||||
mode = "roots_only",
|
||||
totalObjectCount = totalObjectCount,
|
||||
rootCount = roots.Count,
|
||||
roots = roots,
|
||||
hints = new[]
|
||||
{
|
||||
"Use unity_gameobject action='list_children' on a specific root GameObject to expand its subtree with a depth limit.",
|
||||
"Use unity_gameobject action='find' with searchMethod='by_path' to locate a specific object (e.g. 'Root/Child') before listing children.",
|
||||
"If you only need a specific area, pick a root from this list and then drill down incrementally (depth=1..N)."
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
|
||||
|
||||
var resp = Response.Success(
|
||||
$"Retrieved hierarchy for scene '{activeScene.name}'.",
|
||||
hierarchy
|
||||
);
|
||||
try { TcpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { }
|
||||
return resp;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try { TcpLog.Error($"[ManageScene] get_hierarchy: exception {e.Message}"); } catch { }
|
||||
return Response.Error($"Error getting scene hierarchy: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ensure Methods (Idempotent Operations) ---
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a scene is open. Idempotent.
|
||||
/// </summary>
|
||||
private static object EnsureSceneOpen(string scenePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed($"ensure_scene_open({scenePath})");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
if (!File.Exists(scenePath))
|
||||
return Response.Error($"Scene file not found: {scenePath}");
|
||||
|
||||
Scene activeScene = SceneManager.GetActiveScene();
|
||||
if (activeScene.path.Equals(scenePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Scene is already active.",
|
||||
data = new { scenePath = activeScene.path, dirty = activeScene.isDirty },
|
||||
state_delta = StateComposer.CreateSceneDelta(activeScene.path, activeScene.isDirty)
|
||||
};
|
||||
}
|
||||
|
||||
Scene loadedScene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
|
||||
if (!loadedScene.IsValid())
|
||||
return Response.Error($"Failed to open scene: {scenePath}");
|
||||
|
||||
StateComposer.IncrementRevision();
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Scene opened.",
|
||||
data = new { scenePath = loadedScene.path, dirty = loadedScene.isDirty },
|
||||
state_delta = StateComposer.CreateSceneDelta(loadedScene.path, loadedScene.isDirty)
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to ensure scene open: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the active scene is saved. Idempotent.
|
||||
/// </summary>
|
||||
private static object EnsureSceneSaved()
|
||||
{
|
||||
try
|
||||
{
|
||||
Scene activeScene = SceneManager.GetActiveScene();
|
||||
if (!activeScene.IsValid())
|
||||
return Response.Error("No active scene.");
|
||||
|
||||
if (!activeScene.isDirty)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Scene already saved.",
|
||||
data = new { scenePath = activeScene.path, dirty = false },
|
||||
state_delta = StateComposer.CreateSceneDelta(activeScene.path, false)
|
||||
};
|
||||
}
|
||||
|
||||
var writeCheck = WriteGuard.CheckWriteAllowed($"ensure_scene_saved");
|
||||
if (writeCheck != null) return writeCheck;
|
||||
|
||||
bool saved = EditorSceneManager.SaveScene(activeScene);
|
||||
if (!saved)
|
||||
return Response.Error($"Failed to save scene.");
|
||||
|
||||
StateComposer.IncrementRevision();
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Scene saved.",
|
||||
data = new { scenePath = activeScene.path, dirty = false },
|
||||
state_delta = StateComposer.CreateSceneDelta(activeScene.path, false)
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to ensure scene saved: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts total GameObjects in a hierarchy. Uses an iterative traversal to avoid stack overflows on deep hierarchies.
|
||||
/// </summary>
|
||||
private static int CountGameObjectsRecursive(GameObject go)
|
||||
{
|
||||
if (go == null) return 0;
|
||||
int count = 0;
|
||||
var stack = new Stack<Transform>();
|
||||
if (go.transform != null)
|
||||
{
|
||||
stack.Push(go.transform);
|
||||
}
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var tr = stack.Pop();
|
||||
if (tr == null) continue;
|
||||
count++;
|
||||
int cc = tr.childCount;
|
||||
for (int i = 0; i < cc; i++)
|
||||
{
|
||||
var child = tr.GetChild(i);
|
||||
if (child != null) stack.Push(child);
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively builds a data representation of a GameObject and its children.
|
||||
/// </summary>
|
||||
private static object GetGameObjectDataRecursive(GameObject go)
|
||||
{
|
||||
if (go == null)
|
||||
return null;
|
||||
|
||||
var childrenData = new List<object>();
|
||||
foreach (Transform child in go.transform)
|
||||
{
|
||||
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
|
||||
}
|
||||
|
||||
var gameObjectData = new Dictionary<string, object>
|
||||
{
|
||||
{ "name", go.name },
|
||||
{ "activeSelf", go.activeSelf },
|
||||
{ "activeInHierarchy", go.activeInHierarchy },
|
||||
{ "tag", go.tag },
|
||||
{ "layer", go.layer },
|
||||
{ "isStatic", go.isStatic },
|
||||
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
|
||||
{
|
||||
"transform",
|
||||
new
|
||||
{
|
||||
position = new
|
||||
{
|
||||
x = go.transform.localPosition.x,
|
||||
y = go.transform.localPosition.y,
|
||||
z = go.transform.localPosition.z,
|
||||
},
|
||||
rotation = new
|
||||
{
|
||||
x = go.transform.localRotation.eulerAngles.x,
|
||||
y = go.transform.localRotation.eulerAngles.y,
|
||||
z = go.transform.localRotation.eulerAngles.z,
|
||||
}, // Euler for simplicity
|
||||
scale = new
|
||||
{
|
||||
x = go.transform.localScale.x,
|
||||
y = go.transform.localScale.y,
|
||||
z = go.transform.localScale.z,
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "children", childrenData },
|
||||
};
|
||||
|
||||
return gameObjectData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2446b8cefbf129d40abfa286e9e3d137
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,662 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Codely.Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityTcp.Editor.Helpers; // For Response class
|
||||
|
||||
namespace UnityTcp.Editor.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles screenshot capture operations within Unity Editor.
|
||||
/// </summary>
|
||||
public static class ManageScreenshot
|
||||
{
|
||||
// --- Main Handler ---
|
||||
|
||||
// Define the list of valid actions
|
||||
private static readonly List<string> ValidActions = new List<string>
|
||||
{
|
||||
"capture",
|
||||
"capture_main_camera",
|
||||
"capture_specific_camera",
|
||||
"capture_scene_camera"
|
||||
};
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
return Response.Error("Action parameter is required.");
|
||||
}
|
||||
|
||||
// Check if the action is valid before switching
|
||||
if (!ValidActions.Contains(action))
|
||||
{
|
||||
string validActionsList = string.Join(", ", ValidActions);
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions are: {validActionsList}"
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "capture":
|
||||
return CaptureGameView(@params);
|
||||
case "capture_main_camera":
|
||||
return CaptureMainCamera(@params);
|
||||
case "capture_specific_camera":
|
||||
return CaptureSpecificCamera(@params);
|
||||
case "capture_scene_camera":
|
||||
return CaptureSceneCamera(@params);
|
||||
|
||||
default:
|
||||
string validActionsListDefault = string.Join(", ", ValidActions);
|
||||
return Response.Error(
|
||||
$"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[ManageScreenshot] Action '{action}' failed: {e}");
|
||||
return Response.Error(
|
||||
$"Internal error processing action '{action}': {e.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Action Implementations ---
|
||||
|
||||
/// <summary>
|
||||
/// Menu item to capture screenshot from game view
|
||||
/// </summary>
|
||||
public static void CaptureScreenshotMenuItem()
|
||||
{
|
||||
CaptureGameViewAndSave();
|
||||
}
|
||||
|
||||
private static object CaptureGameView(JObject @params)
|
||||
{
|
||||
string customPath = @params["path"]?.ToString();
|
||||
string customFilename = @params["filename"]?.ToString();
|
||||
int? width = @params["width"]?.ToObject<int?>();
|
||||
int? height = @params["height"]?.ToObject<int?>();
|
||||
|
||||
try
|
||||
{
|
||||
string savedPath = CaptureGameViewAndSave(customPath, customFilename, width, height);
|
||||
if (savedPath != null)
|
||||
{
|
||||
return Response.Success(
|
||||
"Screenshot captured successfully from Game view.",
|
||||
new { path = savedPath, camera = "Game View" }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error("Failed to capture screenshot from Game view.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to capture screenshot from Game view: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CaptureMainCamera(JObject @params)
|
||||
{
|
||||
string customPath = @params["path"]?.ToString();
|
||||
string customFilename = @params["filename"]?.ToString();
|
||||
int? width = @params["width"]?.ToObject<int?>();
|
||||
int? height = @params["height"]?.ToObject<int?>();
|
||||
|
||||
try
|
||||
{
|
||||
string savedPath = CaptureAndSave(customPath, customFilename, width, height);
|
||||
if (savedPath != null)
|
||||
{
|
||||
return Response.Success(
|
||||
"Screenshot captured successfully from main camera.",
|
||||
new { path = savedPath, camera = "Main Camera" }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error("Failed to capture screenshot from main camera.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to capture screenshot: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CaptureSpecificCamera(JObject @params)
|
||||
{
|
||||
string cameraName = @params["cameraName"]?.ToString();
|
||||
string customPath = @params["path"]?.ToString();
|
||||
string customFilename = @params["filename"]?.ToString();
|
||||
int? width = @params["width"]?.ToObject<int?>();
|
||||
int? height = @params["height"]?.ToObject<int?>();
|
||||
|
||||
if (string.IsNullOrEmpty(cameraName))
|
||||
{
|
||||
return Response.Error("'cameraName' parameter is required for capture_specific_camera action.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Camera targetCamera = GameObject.Find(cameraName)?.GetComponent<Camera>();
|
||||
if (targetCamera == null)
|
||||
{
|
||||
// Try finding by name in all cameras
|
||||
Camera[] cameras = UnityEngine.Object.FindObjectsOfType<Camera>();
|
||||
foreach (Camera cam in cameras)
|
||||
{
|
||||
if (cam.name.Equals(cameraName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
targetCamera = cam;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetCamera == null)
|
||||
{
|
||||
return Response.Error($"Camera '{cameraName}' not found in the scene.");
|
||||
}
|
||||
|
||||
string savedPath = CaptureAndSave(customPath, customFilename, width, height, targetCamera);
|
||||
if (savedPath != null)
|
||||
{
|
||||
return Response.Success(
|
||||
$"Screenshot captured successfully from camera '{cameraName}'.",
|
||||
new { path = savedPath, camera = cameraName }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error($"Failed to capture screenshot from camera '{cameraName}'.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to capture screenshot from camera '{cameraName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static object CaptureSceneCamera(JObject @params)
|
||||
{
|
||||
string customPath = @params["path"]?.ToString();
|
||||
string customFilename = @params["filename"]?.ToString();
|
||||
int? width = @params["width"]?.ToObject<int?>();
|
||||
int? height = @params["height"]?.ToObject<int?>();
|
||||
|
||||
try
|
||||
{
|
||||
// Get the active scene view camera
|
||||
SceneView sceneView = SceneView.lastActiveSceneView;
|
||||
if (sceneView == null)
|
||||
{
|
||||
return Response.Error("No active Scene view found. Please ensure a Scene view is open.");
|
||||
}
|
||||
|
||||
Camera sceneCamera = sceneView.camera;
|
||||
if (sceneCamera == null)
|
||||
{
|
||||
return Response.Error("Scene view camera not found.");
|
||||
}
|
||||
|
||||
string savedPath = CaptureAndSave(customPath, customFilename, width, height, sceneCamera, "SceneCamera");
|
||||
if (savedPath != null)
|
||||
{
|
||||
return Response.Success(
|
||||
"Screenshot captured successfully from Scene camera.",
|
||||
new { path = savedPath, camera = "Scene Camera" }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Response.Error("Failed to capture screenshot from Scene camera.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return Response.Error($"Failed to capture screenshot from Scene camera: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function to capture Game view screenshot and save to file
|
||||
/// </summary>
|
||||
public static string CaptureGameViewAndSave(string customPath = null, string customFilename = null, int? width = null, int? height = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Generate filename with GameView prefix
|
||||
string filename;
|
||||
if (!string.IsNullOrEmpty(customFilename))
|
||||
{
|
||||
filename = customFilename.EndsWith(".png") ? customFilename : customFilename + ".png";
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = $"GameView-{System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.png";
|
||||
}
|
||||
|
||||
// Determine save path
|
||||
string savePath;
|
||||
if (!string.IsNullOrEmpty(customPath))
|
||||
{
|
||||
// Ensure directory exists
|
||||
Directory.CreateDirectory(customPath);
|
||||
savePath = Path.Combine(customPath, filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use project path with Screenshots folder
|
||||
string projectPath = Path.GetDirectoryName(Application.dataPath); // Get project root (parent of Assets)
|
||||
string screenshotsFolder = Path.Combine(projectPath, "Screenshots");
|
||||
|
||||
// Create Screenshots folder if it doesn't exist
|
||||
if (!Directory.Exists(screenshotsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(screenshotsFolder);
|
||||
Debug.Log($"Created Screenshots folder at: {screenshotsFolder}");
|
||||
}
|
||||
|
||||
savePath = Path.Combine(screenshotsFolder, filename);
|
||||
}
|
||||
|
||||
// Try different capture methods based on mode and availability
|
||||
Texture2D screenshot = null;
|
||||
|
||||
// Always prioritize GameView reflection to capture what user actually sees
|
||||
// This ensures screenshots match the GameView dimensions, multi-camera setups, and post-processing
|
||||
screenshot = CaptureGameViewInEditMode(width, height);
|
||||
|
||||
if (screenshot == null)
|
||||
{
|
||||
// Fallback to main camera rendering if GameView is not accessible
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera != null)
|
||||
{
|
||||
screenshot = CaptureScreenByRenderTexture(mainCamera, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenshot == null)
|
||||
{
|
||||
Debug.LogError("Failed to capture screenshot using all available methods!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// ReadPixels from RenderTexture often returns bottom-up (OpenGL origin); flip so saved PNG is top-down
|
||||
FlipTextureVertically(screenshot);
|
||||
|
||||
// Encode to PNG and save
|
||||
byte[] bytes = screenshot.EncodeToPNG();
|
||||
UnityEngine.Object.DestroyImmediate(screenshot); // Free GPU memory (must use DestroyImmediate in edit mode)
|
||||
|
||||
File.WriteAllBytes(savePath, bytes);
|
||||
Debug.Log("Game view screenshot saved to: " + savePath);
|
||||
return savePath;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to capture game view screenshot: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture screenshot from camera using RenderTexture approach
|
||||
/// </summary>
|
||||
private static Texture2D CaptureScreenByRenderTexture(Camera camera, int? width = null, int? height = null)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
Debug.LogError("Camera is null!");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use custom dimensions or default to reasonable size
|
||||
int captureWidth = width ?? 1920;
|
||||
int captureHeight = height ?? 1080;
|
||||
|
||||
Rect rect = new Rect(0, 0, captureWidth, captureHeight);
|
||||
|
||||
// Create a RenderTexture object
|
||||
RenderTexture rt = new RenderTexture((int)rect.width, (int)rect.height, 24);
|
||||
|
||||
// Store original target texture to restore later
|
||||
RenderTexture originalTarget = camera.targetTexture;
|
||||
|
||||
// Temporarily set the camera's targetTexture to rt, and manually render the camera
|
||||
camera.targetTexture = rt;
|
||||
camera.Render();
|
||||
|
||||
// Activate this rt, and read pixels from it.
|
||||
RenderTexture previousActive = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
|
||||
Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
|
||||
// Note: At this time, it reads pixels from RenderTexture.active
|
||||
screenShot.ReadPixels(rect, 0, 0);
|
||||
screenShot.Apply();
|
||||
|
||||
// Reset related parameters so the camera continues to display on screen
|
||||
camera.targetTexture = originalTarget;
|
||||
RenderTexture.active = previousActive;
|
||||
UnityEngine.Object.DestroyImmediate(rt);
|
||||
|
||||
return screenShot;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to capture screenshot using RenderTexture: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips the texture vertically in-place. Use before saving when the image is upside-down.
|
||||
/// ReadPixels from RenderTexture uses bottom-left origin (e.g. OpenGL); PNG expects top-left,
|
||||
/// so screenshots from GameView/camera RT can appear flipped. Call this before EncodeToPNG to correct.
|
||||
/// </summary>
|
||||
private static void FlipTextureVertically(Texture2D tex)
|
||||
{
|
||||
if (tex == null || tex.height <= 1) return;
|
||||
int w = tex.width;
|
||||
int h = tex.height;
|
||||
Color[] pixels = tex.GetPixels();
|
||||
for (int y = 0; y < h / 2; y++)
|
||||
{
|
||||
int top = y * w;
|
||||
int bottom = (h - 1 - y) * w;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
Color tmp = pixels[top + x];
|
||||
pixels[top + x] = pixels[bottom + x];
|
||||
pixels[bottom + x] = tmp;
|
||||
}
|
||||
}
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture Game view using GameView's internal render texture via reflection
|
||||
/// Works in both Edit and Play modes to capture what the user actually sees.
|
||||
/// Note: ReadPixels from RT may yield bottom-up image (OpenGL); save path flips vertically before PNG.
|
||||
/// </summary>
|
||||
private static Texture2D CaptureGameViewInEditMode(int? width = null, int? height = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get active GameView
|
||||
var gameView = GetActiveGameView();
|
||||
if (gameView == null)
|
||||
{
|
||||
Debug.LogWarning("Game View not found. Falling back to camera rendering.");
|
||||
return CaptureWithCameraRenderingFallback(width, height);
|
||||
}
|
||||
|
||||
// Try to get GameView's internal render texture via reflection
|
||||
// Different Unity versions may use different property/field names
|
||||
RenderTexture gameViewRT = null;
|
||||
var gameViewType = gameView.GetType();
|
||||
|
||||
// Try property "targetTexture" (some Unity versions)
|
||||
var textureProperty = gameViewType.GetProperty("targetTexture",
|
||||
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
if (textureProperty != null)
|
||||
{
|
||||
gameViewRT = textureProperty.GetValue(gameView, null) as RenderTexture;
|
||||
}
|
||||
|
||||
// Try field "m_TargetTexture" (some Unity versions)
|
||||
if (gameViewRT == null)
|
||||
{
|
||||
var textureField = gameViewType.GetField("m_TargetTexture",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (textureField != null)
|
||||
{
|
||||
gameViewRT = textureField.GetValue(gameView) as RenderTexture;
|
||||
}
|
||||
}
|
||||
|
||||
// Try method "GetMainPlayModeView" or similar approaches
|
||||
if (gameViewRT == null)
|
||||
{
|
||||
var renderDocField = gameViewType.GetField("m_RenderTexture",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (renderDocField != null)
|
||||
{
|
||||
gameViewRT = renderDocField.GetValue(gameView) as RenderTexture;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameViewRT == null)
|
||||
{
|
||||
Debug.LogWarning("GameView render texture not accessible. Falling back to camera rendering.");
|
||||
return CaptureWithCameraRenderingFallback(width, height);
|
||||
}
|
||||
|
||||
RenderTexture activeRT = RenderTexture.active;
|
||||
|
||||
// Determine final dimensions
|
||||
int finalWidth = width ?? gameViewRT.width;
|
||||
int finalHeight = height ?? gameViewRT.height;
|
||||
|
||||
// Create temporary RenderTexture to read pixels
|
||||
RenderTexture tempRT;
|
||||
|
||||
// If custom dimensions are specified and different from game view, resize
|
||||
if ((width.HasValue || height.HasValue) && (finalWidth != gameViewRT.width || finalHeight != gameViewRT.height))
|
||||
{
|
||||
tempRT = RenderTexture.GetTemporary(finalWidth, finalHeight, 0, gameViewRT.format);
|
||||
// Scale the game view texture to the desired size
|
||||
Graphics.Blit(gameViewRT, tempRT);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use game view dimensions
|
||||
tempRT = RenderTexture.GetTemporary(gameViewRT.width, gameViewRT.height, 0, gameViewRT.format);
|
||||
Graphics.Blit(gameViewRT, tempRT);
|
||||
}
|
||||
|
||||
RenderTexture.active = tempRT;
|
||||
Texture2D screenshot = new Texture2D(tempRT.width, tempRT.height, TextureFormat.RGB24, false);
|
||||
screenshot.ReadPixels(new Rect(0, 0, tempRT.width, tempRT.height), 0, 0);
|
||||
screenshot.Apply();
|
||||
|
||||
RenderTexture.active = activeRT;
|
||||
RenderTexture.ReleaseTemporary(tempRT);
|
||||
|
||||
return screenshot;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Failed to capture game view using reflection: {e.Message}. Falling back to camera rendering.");
|
||||
return CaptureWithCameraRenderingFallback(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the active Game View window using reflection
|
||||
/// </summary>
|
||||
private static EditorWindow GetActiveGameView()
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Type gameViewType = System.Type.GetType("UnityEditor.GameView,UnityEditor");
|
||||
if (gameViewType == null)
|
||||
return null;
|
||||
|
||||
// Try to get the focused game view first
|
||||
EditorWindow focusedWindow = EditorWindow.focusedWindow;
|
||||
if (focusedWindow != null && focusedWindow.GetType() == gameViewType)
|
||||
return focusedWindow;
|
||||
|
||||
// If no focused game view, get any game view that exists
|
||||
EditorWindow[] gameViews = Resources.FindObjectsOfTypeAll(gameViewType) as EditorWindow[];
|
||||
if (gameViews != null && gameViews.Length > 0)
|
||||
return gameViews[0];
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogWarning($"Could not get Game View: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback method using camera rendering when GameView access fails
|
||||
/// </summary>
|
||||
private static Texture2D CaptureWithCameraRenderingFallback(int? width = null, int? height = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
int captureWidth = width ?? Screen.width;
|
||||
int captureHeight = height ?? Screen.height;
|
||||
|
||||
// Create render texture
|
||||
RenderTexture rt = new RenderTexture(captureWidth, captureHeight, 24);
|
||||
|
||||
// Get all cameras and render them to the render texture
|
||||
Camera[] cameras = Camera.allCameras;
|
||||
RenderTexture previousActive = RenderTexture.active;
|
||||
RenderTexture.active = rt;
|
||||
|
||||
// Clear the render texture
|
||||
GL.Clear(true, true, Color.black);
|
||||
|
||||
// Render all active cameras to the render texture in order
|
||||
foreach (Camera cam in cameras)
|
||||
{
|
||||
if (cam != null && cam.enabled && cam.gameObject.activeInHierarchy)
|
||||
{
|
||||
RenderTexture prevTarget = cam.targetTexture;
|
||||
cam.targetTexture = rt;
|
||||
cam.Render();
|
||||
cam.targetTexture = prevTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// Read pixels from render texture
|
||||
Texture2D screenshot = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false);
|
||||
screenshot.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0);
|
||||
screenshot.Apply();
|
||||
|
||||
// Cleanup
|
||||
RenderTexture.active = previousActive;
|
||||
UnityEngine.Object.DestroyImmediate(rt);
|
||||
|
||||
return screenshot;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"Camera rendering fallback failed: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to resize a texture
|
||||
/// </summary>
|
||||
private static Texture2D ResizeTexture(Texture2D source, int targetWidth, int targetHeight)
|
||||
{
|
||||
RenderTexture rt = RenderTexture.GetTemporary(targetWidth, targetHeight);
|
||||
RenderTexture.active = rt;
|
||||
Graphics.Blit(source, rt);
|
||||
|
||||
Texture2D result = new Texture2D(targetWidth, targetHeight);
|
||||
result.ReadPixels(new Rect(0, 0, targetWidth, targetHeight), 0, 0);
|
||||
result.Apply();
|
||||
|
||||
RenderTexture.active = null;
|
||||
RenderTexture.ReleaseTemporary(rt);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function to capture screenshot and save to file
|
||||
/// </summary>
|
||||
public static string CaptureAndSave(string customPath = null, string customFilename = null, int? width = null, int? height = null, Camera specificCamera = null, string cameraPrefix = null)
|
||||
{
|
||||
// Get the camera to use
|
||||
Camera cam = specificCamera ?? Camera.main;
|
||||
if (cam == null)
|
||||
{
|
||||
Debug.LogError("No main camera found!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the new RenderTexture approach
|
||||
Texture2D screenshot = CaptureScreenByRenderTexture(cam, width, height);
|
||||
if (screenshot == null)
|
||||
{
|
||||
Debug.LogError("Failed to capture screenshot using RenderTexture!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Encode to PNG
|
||||
byte[] bytes = screenshot.EncodeToPNG();
|
||||
UnityEngine.Object.DestroyImmediate(screenshot);
|
||||
|
||||
// Generate filename with camera prefix
|
||||
string filename;
|
||||
if (!string.IsNullOrEmpty(customFilename))
|
||||
{
|
||||
filename = customFilename.EndsWith(".png") ? customFilename : customFilename + ".png";
|
||||
}
|
||||
else
|
||||
{
|
||||
string prefix = cameraPrefix ?? (specificCamera != null ? specificCamera.name : "MainCamera");
|
||||
filename = $"{prefix}-{System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.png";
|
||||
}
|
||||
|
||||
// Determine save path
|
||||
string savePath;
|
||||
if (!string.IsNullOrEmpty(customPath))
|
||||
{
|
||||
// Ensure directory exists
|
||||
Directory.CreateDirectory(customPath);
|
||||
savePath = Path.Combine(customPath, filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use project path with Screenshots folder
|
||||
string projectPath = Path.GetDirectoryName(Application.dataPath); // Get project root (parent of Assets)
|
||||
string screenshotsFolder = Path.Combine(projectPath, "Screenshots");
|
||||
|
||||
// Create Screenshots folder if it doesn't exist
|
||||
if (!Directory.Exists(screenshotsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(screenshotsFolder);
|
||||
Debug.Log($"Created Screenshots folder at: {screenshotsFolder}");
|
||||
}
|
||||
|
||||
savePath = Path.Combine(screenshotsFolder, filename);
|
||||
}
|
||||
|
||||
// Save to file
|
||||
File.WriteAllBytes(savePath, bytes);
|
||||
|
||||
Debug.Log("Screenshot saved to: " + savePath);
|
||||
return savePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fd45617bc3cd52489b0ad49fe49e55b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||