修改提交

This commit is contained in:
Bob.Song
2026-03-09 17:50:20 +08:00
parent 68beeb3417
commit 27b85fd875
228 changed files with 30829 additions and 1509 deletions

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4cb996b11fa557143a68a6fdb4f0cb76
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ff37f8f7a26ddcf4a8ec576dd133285c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("UnityTcpTests.EditMode")]

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7083f648bddd50d41afc42cc3deb2577
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 626ae8824a7df62489152ef051a0e2a9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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());
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ccdab4cb337ac174395ef110bfa8f3b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9addeba67f678854eada157f672b975d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96c791cc231905b4ca2231ba84bc1f4f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4a955ce7b85e184597177720c6d46b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29be5222623c0324ca01f6b7ffaaa602
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6a17149005eb51642ab32aa65be61cc7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f24f49a4ec33ffe448b5c69d381dfa9a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db27ee87f1c170b47928576e31cc2c9a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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 };
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 635f93405037a114993e3a9a44c54745
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e6177725a55072419d7584603153d01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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";
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12eba3a4a35da834eba2ca7f63decd97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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>");
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: a1da5b6ee62708c4a9df5c2a0f624f1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75e6d1e08c44ad84d91f82a4baeea37e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 15dbae89d2b872442a3021294af9f5bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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]);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8514fd42f23cb641a36e52550825b35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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)"
}
}
});
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 593f57416aee1924495c4971200b544b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3d6ea1a77c9612d4e951e572b2ce2dbb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

View 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

View 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:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0f0829f6381c0ab41b63ef97b48a59cd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1eb3a812e461dfa499f91f9500604ef3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f0bd257c0ef5f4448dc88dfe19562c4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8301b99c61e7b7e44befcf784c72a226
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
*/
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 333f972789d338c4aafe2236cb5ac3cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a919bae4d47922248a206faa7ba67ed7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ef1416a06c8bce4caaff3a2b88aaafe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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 ... }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 896e8045986eb0d449ee68395479f1d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d58d03981d97594b80e043a0ba78f55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16bc91f90f3df674486759e40dffb088
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da88809c82aa8214eaa168ec9ce090af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c6230a18f5554a41aaac2188776332e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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}");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d06adc1d5b654a428a23760d94d5079
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2446b8cefbf129d40abfa286e9e3d137
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fd45617bc3cd52489b0ad49fe49e55b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More