using System; using System.Collections.Generic; using System.Linq; using System.IO; using Codely.Newtonsoft.Json.Linq; using UnityEditor; using UnityEditorInternal; // Required for tag management using UnityEngine; using UnityTcp.Editor.Helpers; // For Response class namespace UnityTcp.Editor.Tools { /// /// Handles operations related to controlling and querying the Unity Editor state, /// including managing Tags and Layers, and compilation workflow. /// Compatible with Unity 2022.3 LTS. /// public static class ManageEditor { // Constant for starting user layer index private const int FirstUserLayerIndex = 8; // Constant for total layer count private const int TotalLayerCount = 32; // Compilation event tracking private static bool _compilationCallbackRegistered = false; private static readonly object _compilationLock = new object(); // Idle wait tracking private static readonly Dictionary _idleCallbacks = new Dictionary(); private static readonly object _idleLock = new object(); /// /// Main handler for editor management actions. /// public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); // Parameters for specific actions string tagName = @params["tagName"]?.ToString(); string layerName = @params["layerName"]?.ToString(); bool waitForCompletion = @params["waitForCompletion"]?.ToObject() ?? false; // Example - not used everywhere if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } // Ensure compilation callbacks are registered EnsureCompilationCallbacksRegistered(); // Route action switch (action) { // Full State Retrieval (API Spec aligned) case "get_current_state": return GetCurrentState(); // Compilation Management case "request_compile": return RequestCompile(); case "start_compilation_pipeline": // Standard pipeline: clear console → request compile → return token return CompilationHelper.StartCompilationPipeline(); case "wait_for_compile": string opId = @params["op_id"]?.ToString(); int timeoutSeconds = @params["timeoutSeconds"]?.ToObject() ?? 60; string sinceToken = @params["since_token"]?.ToString(); if (string.IsNullOrEmpty(opId)) return Response.Error("'op_id' parameter required for wait_for_compile."); return WaitForCompile(opId, timeoutSeconds, sinceToken); case "get_compilation_summary": return CompilationHelper.GetCompilationSummary(); case "wait_for_idle": int idleTimeout = @params["timeoutSeconds"]?.ToObject() ?? 600; return WaitForIdle(idleTimeout); // Play Mode Control case "play": try { if (!EditorApplication.isPlaying) { EditorApplication.isPlaying = true; // Include updated playMode so clients can sync state return Response.Success("Entered play mode.", new { playMode = "playing" }); } return Response.Success("Already in play mode.", new { playMode = "playing" }); } catch (Exception e) { return Response.Error($"Error entering play mode: {e.Message}"); } case "pause": try { if (EditorApplication.isPlaying) { EditorApplication.isPaused = !EditorApplication.isPaused; var isPaused = EditorApplication.isPaused; return Response.Success( isPaused ? "Game paused." : "Game resumed.", new { playMode = isPaused ? "paused" : "playing" } ); } return Response.Error("Cannot pause/resume: Not in play mode."); } catch (Exception e) { return Response.Error($"Error pausing/resuming game: {e.Message}"); } case "stop": try { if (EditorApplication.isPlaying) { EditorApplication.isPlaying = false; return Response.Success("Exited play mode.", new { playMode = "stopped" }); } return Response.Success("Already stopped (not in play mode).", new { playMode = "stopped" }); } catch (Exception e) { return Response.Error($"Error stopping play mode: {e.Message}"); } // Editor State/Info case "get_state": return GetEditorState(); case "get_project_root": return GetProjectRoot(); case "get_windows": return GetEditorWindows(); case "get_active_tool": return GetActiveTool(); case "get_selection": return GetSelection(); case "set_active_tool": string toolName = @params["toolName"]?.ToString(); if (string.IsNullOrEmpty(toolName)) return Response.Error("'toolName' parameter required for set_active_tool."); return SetActiveTool(toolName); // Tag Management case "ensure_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for ensure_tag."); return EnsureTag(tagName); case "add_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for add_tag."); return AddTag(tagName); case "remove_tag": if (string.IsNullOrEmpty(tagName)) return Response.Error("'tagName' parameter required for remove_tag."); return RemoveTag(tagName); case "get_tags": return GetTags(); // Helper to list current tags // Layer Management case "ensure_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for ensure_layer."); return EnsureLayer(layerName); case "add_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for add_layer."); return AddLayer(layerName); case "remove_layer": if (string.IsNullOrEmpty(layerName)) return Response.Error("'layerName' parameter required for remove_layer."); return RemoveLayer(layerName); case "get_layers": return GetLayers(); // Helper to list current layers // Window Focus case "focus_window": string windowType = @params["windowType"]?.ToString(); if (string.IsNullOrEmpty(windowType)) return Response.Error("'windowType' parameter required for focus_window."); return FocusWindow(windowType); // --- Settings (Example) --- // case "set_resolution": // int? width = @params["width"]?.ToObject(); // int? height = @params["height"]?.ToObject(); // if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required."); // return SetGameViewResolution(width.Value, height.Value); // case "set_quality": // // Handle string name or int index // return SetQualityLevel(@params["qualityLevel"]); default: return Response.Error( $"Unknown action: '{action}'. Supported actions include: get_current_state, request_compile, wait_for_compile, wait_for_idle, play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers, focus_window." ); } } // --- Full State Retrieval --- /// /// Returns the complete UnityCurrentState snapshot. /// This is the primary entry point for LLMs to understand the full editor state. /// private static object GetCurrentState() { try { // Build state and increment revision atomically var fullState = StateComposer.BuildFullStateAndIncrement(); return new { success = true, message = "Retrieved full Unity state snapshot.", state = fullState }; } catch (Exception e) { return Response.Error($"Error getting current state: {e.Message}"); } } // --- Editor State/Info Methods --- private static object GetEditorState() { try { // Use StateComposer to build comprehensive state var fullState = StateComposer.BuildFullState(); // Also include legacy fields for backward compatibility var legacyData = new { isPlaying = EditorApplication.isPlaying, isPaused = EditorApplication.isPaused, isCompiling = EditorApplication.isCompiling, isUpdating = EditorApplication.isUpdating, applicationPath = EditorApplication.applicationPath, applicationContentsPath = EditorApplication.applicationContentsPath, timeSinceStartup = EditorApplication.timeSinceStartup, }; return new { success = true, message = "Retrieved editor state.", data = legacyData, state = fullState // NEW: Full state snapshot }; } catch (Exception e) { return Response.Error($"Error getting editor state: {e.Message}"); } } private static object GetProjectRoot() { try { // Application.dataPath points to /Assets string assetsPath = Application.dataPath.Replace('\\', '/'); string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); if (string.IsNullOrEmpty(projectRoot)) { return Response.Error("Could not determine project root from Application.dataPath"); } return Response.Success("Project root resolved.", new { projectRoot }); } catch (Exception e) { return Response.Error($"Error getting project root: {e.Message}"); } } private static object GetEditorWindows() { try { // Get all types deriving from EditorWindow var windowTypes = AppDomain .CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(type => type.IsSubclassOf(typeof(EditorWindow))) .ToList(); var openWindows = new List(); // Find currently open instances // Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); foreach (EditorWindow window in allWindows) { if (window == null) continue; // Skip potentially destroyed windows try { openWindows.Add( new { title = window.titleContent.text, typeName = window.GetType().FullName, isFocused = EditorWindow.focusedWindow == window, position = new { x = window.position.x, y = window.position.y, width = window.position.width, height = window.position.height, }, instanceID = window.GetInstanceID(), } ); } catch (Exception ex) { Debug.LogWarning( $"Could not get info for window {window.GetType().Name}: {ex.Message}" ); } } return Response.Success("Retrieved list of open editor windows.", openWindows); } catch (Exception e) { return Response.Error($"Error getting editor windows: {e.Message}"); } } /// /// Focuses an editor window by type name. /// Supports common window names like "Console", "Inspector", "Hierarchy", "Project", "Scene", "Game". /// private static object FocusWindow(string windowType) { try { // Map common names to actual EditorWindow type names var typeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Console", "UnityEditor.ConsoleWindow" }, { "Inspector", "UnityEditor.InspectorWindow" }, { "Hierarchy", "UnityEditor.SceneHierarchyWindow" }, { "Project", "UnityEditor.ProjectBrowser" }, { "Scene", "UnityEditor.SceneView" }, { "Game", "UnityEditor.GameView" }, { "Animator", "UnityEditor.Graphs.AnimatorControllerTool" }, { "Animation", "UnityEditor.AnimationWindow" }, { "Profiler", "UnityEditor.ProfilerWindow" }, { "AssetStore", "UnityEditor.AssetStoreWindow" }, { "PackageManager", "UnityEditor.PackageManager.UI.PackageManagerWindow" }, { "Build", "UnityEditor.BuildPlayerWindow" }, { "Lighting", "UnityEditor.LightingWindow" }, { "Navigation", "UnityEditor.NavMeshEditorWindow" }, { "Occlusion", "UnityEditor.OcclusionCullingWindow" }, { "FrameDebugger", "UnityEditor.FrameDebuggerWindow" }, { "AudioMixer", "UnityEditor.AudioMixerWindow" } }; string fullTypeName = windowType; if (typeMap.TryGetValue(windowType, out var mappedType)) { fullTypeName = mappedType; } // Find all open windows EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll(); EditorWindow targetWindow = null; foreach (EditorWindow window in allWindows) { if (window == null) continue; var winTypeName = window.GetType().FullName; // Match by full type name, short type name, or title if (winTypeName.Equals(fullTypeName, StringComparison.OrdinalIgnoreCase) || winTypeName.EndsWith("." + windowType, StringComparison.OrdinalIgnoreCase) || window.GetType().Name.Equals(windowType, StringComparison.OrdinalIgnoreCase) || window.titleContent.text.Equals(windowType, StringComparison.OrdinalIgnoreCase)) { targetWindow = window; break; } } if (targetWindow == null) { // Try to open the window if it's a known type Type windowTypeObj = null; foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { windowTypeObj = assembly.GetType(fullTypeName); if (windowTypeObj != null) break; } if (windowTypeObj != null && typeof(EditorWindow).IsAssignableFrom(windowTypeObj)) { // Use GetWindow to open it targetWindow = EditorWindow.GetWindow(windowTypeObj); } else { return Response.Error( $"Window '{windowType}' not found. Available windows can be queried with get_windows action. " + $"Common window types: Console, Inspector, Hierarchy, Project, Scene, Game, Animator, Animation, Profiler." ); } } // Focus the window targetWindow.Focus(); // Verify focus was successful bool isFocused = EditorWindow.focusedWindow == targetWindow; return Response.Success( $"Focused window: {targetWindow.titleContent.text} ({targetWindow.GetType().Name})", new { windowType = targetWindow.GetType().FullName, title = targetWindow.titleContent.text, isFocused = isFocused, instanceID = targetWindow.GetInstanceID() } ); } catch (Exception e) { return Response.Error($"Error focusing window '{windowType}': {e.Message}"); } } private static object GetActiveTool() { try { Tool currentTool = UnityEditor.Tools.current; string toolName = currentTool.ToString(); // Enum to string bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; // Get custom name if needed // Convert Unity types to serializable arrays to avoid self-referencing loop var handleRot = UnityEditor.Tools.handleRotation.eulerAngles; var handlePos = UnityEditor.Tools.handlePosition; var toolInfo = new { activeTool = activeToolName, isCustom = customToolActive, pivotMode = UnityEditor.Tools.pivotMode.ToString(), pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), handleRotation = new float[] { handleRot.x, handleRot.y, handleRot.z }, handlePosition = new float[] { handlePos.x, handlePos.y, handlePos.z }, }; return Response.Success("Retrieved active tool information.", toolInfo); } catch (Exception e) { return Response.Error($"Error getting active tool: {e.Message}"); } } private static object SetActiveTool(string toolName) { try { Tool targetTool; if (Enum.TryParse(toolName, true, out targetTool)) // Case-insensitive parse { // Check if it's a valid built-in tool if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool { UnityEditor.Tools.current = targetTool; return Response.Success($"Set active tool to '{targetTool}'."); } else { return Response.Error( $"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid." ); } } else { // Potentially try activating a custom tool by name here if needed // This often requires specific editor scripting knowledge for that tool. return Response.Error( $"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)." ); } } catch (Exception e) { return Response.Error($"Error setting active tool: {e.Message}"); } } private static object GetSelection() { try { var selectionInfo = new { activeObject = Selection.activeObject?.name, activeGameObject = Selection.activeGameObject?.name, activeTransform = Selection.activeTransform?.name, activeInstanceID = Selection.activeInstanceID, count = Selection.count, objects = Selection .objects.Select(obj => new { name = obj?.name, type = obj?.GetType().FullName, instanceID = obj?.GetInstanceID(), }) .ToList(), gameObjects = Selection .gameObjects.Select(go => new { name = go?.name, instanceID = go?.GetInstanceID(), }) .ToList(), assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view }; return Response.Success("Retrieved current selection details.", selectionInfo); } catch (Exception e) { return Response.Error($"Error getting selection: {e.Message}"); } } // --- Tag Management Methods --- private static object AddTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); // Check if tag already exists if (InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' already exists."); } try { // Add the tag using the internal utility InternalEditorUtility.AddTag(tagName); // Force save assets to ensure the change persists in the TagManager asset AssetDatabase.SaveAssets(); StateComposer.IncrementRevision(); return Response.Success($"Tag '{tagName}' added successfully."); } catch (Exception e) { return Response.Error($"Failed to add tag '{tagName}': {e.Message}"); } } /// /// Idempotent ensure tag - adds tag if not exists, returns success if already exists. /// private static object EnsureTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); // Check if tag already exists if (InternalEditorUtility.tags.Contains(tagName)) { return new { success = true, message = $"Tag '{tagName}' already exists.", data = new { tagName = tagName, alreadyExists = true } }; } // Tag doesn't exist, add it try { InternalEditorUtility.AddTag(tagName); AssetDatabase.SaveAssets(); StateComposer.IncrementRevision(); return new { success = true, message = $"Tag '{tagName}' created successfully.", data = new { tagName = tagName, alreadyExists = false } }; } catch (Exception e) { return Response.Error($"Failed to ensure tag '{tagName}': {e.Message}"); } } private static object RemoveTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return Response.Error("Tag name cannot be empty or whitespace."); if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase)) return Response.Error("Cannot remove the built-in 'Untagged' tag."); // Check if tag exists before attempting removal if (!InternalEditorUtility.tags.Contains(tagName)) { return Response.Error($"Tag '{tagName}' does not exist."); } try { // Remove the tag using the internal utility InternalEditorUtility.RemoveTag(tagName); // Force save assets AssetDatabase.SaveAssets(); return Response.Success($"Tag '{tagName}' removed successfully."); } catch (Exception e) { // Catch potential issues if the tag is somehow in use or removal fails return Response.Error($"Failed to remove tag '{tagName}': {e.Message}"); } } private static object GetTags() { try { string[] tags = InternalEditorUtility.tags; return Response.Success("Retrieved current tags.", tags); } catch (Exception e) { return Response.Error($"Failed to retrieve tags: {e.Message}"); } } // --- Layer Management Methods --- private static object AddLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Check if layer name already exists (case-insensitive check recommended) for (int i = 0; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { return Response.Error($"Layer '{layerName}' already exists at index {i}."); } } // Find the first empty user layer slot (indices 8 to 31) int firstEmptyUserLayer = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) { firstEmptyUserLayer = i; break; } } if (firstEmptyUserLayer == -1) { return Response.Error("No empty User Layer slots available (8-31 are full)."); } // Assign the name to the found slot try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( firstEmptyUserLayer ); targetLayerSP.stringValue = layerName; // Apply the changes to the TagManager asset tagManager.ApplyModifiedProperties(); // Save assets to make sure it's written to disk AssetDatabase.SaveAssets(); StateComposer.IncrementRevision(); return Response.Success( $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." ); } catch (Exception e) { return Response.Error($"Failed to add layer '{layerName}': {e.Message}"); } } /// /// Idempotent ensure layer - adds layer if not exists, returns success if already exists. /// private static object EnsureLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Check if layer already exists for (int i = 0; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if (layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)) { return new { success = true, message = $"Layer '{layerName}' already exists at index {i}.", data = new { layerName = layerName, layerIndex = i, alreadyExists = true } }; } } // Find first empty user layer slot int firstEmptyUserLayer = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) { firstEmptyUserLayer = i; break; } } if (firstEmptyUserLayer == -1) { return Response.Error("No empty User Layer slots available (8-31 are full)."); } // Add the layer try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(firstEmptyUserLayer); targetLayerSP.stringValue = layerName; tagManager.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); StateComposer.IncrementRevision(); return new { success = true, message = $"Layer '{layerName}' created at slot {firstEmptyUserLayer}.", data = new { layerName = layerName, layerIndex = firstEmptyUserLayer, alreadyExists = false } }; } catch (Exception e) { return Response.Error($"Failed to ensure layer '{layerName}': {e.Message}"); } } private static object RemoveLayer(string layerName) { if (string.IsNullOrWhiteSpace(layerName)) return Response.Error("Layer name cannot be empty or whitespace."); // Access the TagManager asset SerializedObject tagManager = GetTagManager(); if (tagManager == null) return Response.Error("Could not access TagManager asset."); SerializedProperty layersProp = tagManager.FindProperty("layers"); if (layersProp == null || !layersProp.isArray) return Response.Error("Could not find 'layers' property in TagManager."); // Find the layer by name (must be user layer) int layerIndexToRemove = -1; for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers { SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); // Case-insensitive comparison is safer if ( layerSP != null && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) ) { layerIndexToRemove = i; break; } } if (layerIndexToRemove == -1) { return Response.Error($"User layer '{layerName}' not found."); } // Clear the name for that index try { SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( layerIndexToRemove ); targetLayerSP.stringValue = string.Empty; // Set to empty string to remove // Apply the changes tagManager.ApplyModifiedProperties(); // Save assets AssetDatabase.SaveAssets(); return Response.Success( $"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully." ); } catch (Exception e) { return Response.Error($"Failed to remove layer '{layerName}': {e.Message}"); } } private static object GetLayers() { try { var layers = new Dictionary(); for (int i = 0; i < TotalLayerCount; i++) { string layerName = LayerMask.LayerToName(i); if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names { layers.Add(i, layerName); } } return Response.Success("Retrieved current named layers.", layers); } catch (Exception e) { return Response.Error($"Failed to retrieve layers: {e.Message}"); } } // --- Compilation Management Methods --- /// /// Ensures compilation event callbacks are registered. /// private static void EnsureCompilationCallbacksRegistered() { lock (_compilationLock) { if (!_compilationCallbackRegistered) { UnityEditor.Compilation.CompilationPipeline.compilationStarted += OnCompilationStarted; UnityEditor.Compilation.CompilationPipeline.compilationFinished += OnCompilationFinished; _compilationCallbackRegistered = true; Debug.Log("[ManageEditor] Compilation callbacks registered"); } } } private static void OnCompilationStarted(object obj) { Debug.Log("[ManageEditor] Compilation started"); // Update all pending compilation jobs var pendingJobs = AsyncOperationTracker.GetPendingJobs(AsyncOperationTracker.JobType.Compilation); foreach (var job in pendingJobs) { AsyncOperationTracker.UpdateProgress(job.OpId, 0.5f, "Compiling scripts..."); } } private static void OnCompilationFinished(object obj) { Debug.Log("[ManageEditor] Compilation finished"); // Complete all pending compilation jobs var pendingJobs = AsyncOperationTracker.GetPendingJobs(AsyncOperationTracker.JobType.Compilation); foreach (var job in pendingJobs) { // Get compilation results var errors = CompilationHelper.GetCompilationErrors(); var warnings = CompilationHelper.GetCompilationWarnings(); // IMPORTANT: Do not claim errors/warnings are 0 unless we actually know. var compilationResult = new Dictionary { ["status"] = "completed", }; if (errors.HasValue) compilationResult["errors"] = errors.Value; if (warnings.HasValue) compilationResult["warnings"] = warnings.Value; if (errors.HasValue) compilationResult["success"] = errors.Value == 0; var completionMessage = errors.HasValue ? (errors.Value > 0 ? "Compilation completed with errors" : "Compilation completed successfully") : "Compilation completed (validate via console)"; AsyncOperationTracker.CompleteJob(job.OpId, completionMessage, compilationResult); } } private static object RequestCompile() { try { // Do not allow script compilation while in Play/Paused mode if (EditorApplication.isPlaying || EditorApplication.isPaused) { return Response.Error( "compile_blocked_in_play_mode", new { code = "compile_blocked_in_play_mode", message = "Compilation is not allowed while the editor is in Play/Paused mode. Stop Play mode (unity_editor.stop) before requesting compilation.", playMode = EditorApplication.isPlaying ? "playing" : "paused" } ); } // Create a job for this compilation request var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.Compilation, "Compilation requested" ); // Request script compilation UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); // Return Pending response with op_id and structured pipeline hints var pending = AsyncOperationTracker.CreatePendingResponse(job) as System.Collections.Generic.Dictionary; if (pending != null) { pending["pipeline_kind"] = "compile"; pending["requires_console_validation"] = true; return pending; } return AsyncOperationTracker.CreatePendingResponse(job); } catch (Exception e) { Debug.LogError($"[ManageEditor] Failed to request compilation: {e}"); return Response.Error($"Failed to request compilation: {e.Message}"); } } private static object WaitForCompile(string opId, int timeoutSeconds, string sinceToken = null) { try { var job = AsyncOperationTracker.GetJob(opId); if (job == null) { // Fallback for domain reload / tracker reset: // The compilation may still be in progress (or may have already finished), // but our AsyncOperationTracker job can be lost. In this case, degrade // gracefully instead of hard-failing so clients can continue with // console-based validation. bool isCompiling = CompilationHelper.IsCompiling(); if (isCompiling) { var pendingDelta = StateComposer.CreateCompilationDelta(true, "compiling"); return new Dictionary { ["status"] = "pending", ["poll_interval"] = 1.0, ["op_id"] = opId, ["success"] = true, ["message"] = $"Compilation in progress (operation {opId} not found; tracking may have been reset). Continue polling and validate via console.", ["pipeline_kind"] = "compile", ["requires_console_validation"] = true, ["state_delta"] = pendingDelta }; } // Not compiling anymore – return a complete-ish response with best-effort diagnostics. var errors = CompilationHelper.GetCompilationErrors(); var warnings = CompilationHelper.GetCompilationWarnings(); var compilationResult = new Dictionary { ["status"] = "completed", ["tracking_lost"] = true }; if (errors.HasValue) compilationResult["errors"] = errors.Value; if (warnings.HasValue) compilationResult["warnings"] = warnings.Value; if (errors.HasValue) compilationResult["success"] = errors.Value == 0; var completionMessage = errors.HasValue ? (errors.Value > 0 ? "Compilation completed with errors" : "Compilation completed successfully") : "Compilation completed (validate via console)"; var compilationDelta = StateComposer.CreateCompilationDelta( false, errors.HasValue && errors.Value > 0 ? "failed" : "completed", errors, warnings ); var completeDict = new Dictionary { ["status"] = "complete", ["op_id"] = opId, ["success"] = true, ["message"] = $"{completionMessage} (operation {opId} not found; tracking may have been reset). Validate via console.", ["data"] = compilationResult, ["state_delta"] = compilationDelta, ["pipeline_kind"] = "compile", ["requires_console_validation"] = true }; var currentToken = StateComposer.GetCurrentConsoleToken(); if (!string.IsNullOrEmpty(currentToken)) { completeDict["console_token"] = currentToken; } StateComposer.IncrementRevision(); return completeDict; } if (job.Type != AsyncOperationTracker.JobType.Compilation) { return Response.Error($"Operation {opId} is not a compilation job."); } // Check timeout if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds)) { AsyncOperationTracker.FailJob(opId, $"Compilation timed out after {timeoutSeconds} seconds"); var errorStateDelta = StateComposer.CreateCompilationDelta(false, "timeout"); return AsyncOperationTracker.CreateErrorResponse(job, errorStateDelta); } // Check status switch (job.Status) { case AsyncOperationTracker.JobStatus.Complete: // Get compilation result data int? errors = null; int? warnings = null; if (job.Data != null) { // Prefer dictionary payloads (most tools use these). var dict = job.Data as IDictionary; if (dict != null) { object eVal; if (dict.TryGetValue("errors", out eVal)) { if (eVal is int) errors = (int)eVal; else if (eVal is long) errors = (int)(long)eVal; else { var eni = eVal as int?; if (eni.HasValue) errors = eni.Value; } } object wVal; if (dict.TryGetValue("warnings", out wVal)) { if (wVal is int) warnings = (int)wVal; else if (wVal is long) warnings = (int)(long)wVal; else { var wni = wVal as int?; if (wni.HasValue) warnings = wni.Value; } } } else { // Fallback for anonymous objects. var dataType = job.Data.GetType(); var errorsProp = dataType.GetProperty("errors"); var warningsProp = dataType.GetProperty("warnings"); if (errorsProp != null) { var eVal = errorsProp.GetValue(job.Data); if (eVal is int) errors = (int)eVal; else if (eVal is long) errors = (int)(long)eVal; else { var eni = eVal as int?; if (eni.HasValue) errors = eni.Value; } } if (warningsProp != null) { var wVal = warningsProp.GetValue(job.Data); if (wVal is int) warnings = (int)wVal; else if (wVal is long) warnings = (int)(long)wVal; else { var wni = wVal as int?; if (wni.HasValue) warnings = wni.Value; } } } } // Create compilation state delta var compilationDelta = StateComposer.CreateCompilationDelta( false, // isCompiling errors.HasValue && errors.Value > 0 ? "failed" : "completed", errors, warnings ); // Include console token info and structured pipeline hints in response var completeResponse = AsyncOperationTracker.CreateCompleteResponse(job, compilationDelta); if (completeResponse is Dictionary completeDict) { // Add console token for log reading var currentToken = StateComposer.GetCurrentConsoleToken(); if (!string.IsNullOrEmpty(currentToken)) { completeDict["console_token"] = currentToken; } // Structured hints for downstream tools/LLMs completeDict["pipeline_kind"] = "compile"; completeDict["requires_console_validation"] = true; } StateComposer.IncrementRevision(); return completeResponse; case AsyncOperationTracker.JobStatus.Error: var errorDelta = StateComposer.CreateCompilationDelta(false, "error"); return AsyncOperationTracker.CreateErrorResponse(job, errorDelta); case AsyncOperationTracker.JobStatus.Pending: // Still pending, return pending response with compilation state delta var pendingDelta = StateComposer.CreateCompilationDelta(true, "compiling"); return AsyncOperationTracker.CreatePendingResponse(job, pendingDelta); default: return Response.Error($"Unknown job status: {job.Status}"); } } catch (Exception e) { Debug.LogError($"[ManageEditor] wait_for_compile failed: {e}"); return Response.Error($"Error waiting for compilation: {e.Message}"); } } private static object WaitForIdle(int timeoutSeconds) { try { // Check if editor is currently idle bool isIdle = !EditorApplication.isCompiling && !EditorApplication.isUpdating; if (isIdle) { return Response.Success("Editor is idle.", new { isCompiling = false, isUpdating = false, elapsed = 0 }); } // Create a job to track the wait var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.Custom, "Waiting for editor to become idle..." ); // Create and store callback delegate for proper unsubscription EditorApplication.CallbackFunction callback = () => CheckIdleState(job.OpId, timeoutSeconds); lock (_idleLock) { _idleCallbacks[job.OpId] = callback; } EditorApplication.update += callback; // Return pending - client will need to poll return AsyncOperationTracker.CreatePendingResponse(job); } catch (Exception e) { Debug.LogError($"[ManageEditor] wait_for_idle failed: {e}"); return Response.Error($"Error waiting for idle: {e.Message}"); } } private static void CheckIdleState(string opId, int timeoutSeconds) { var job = AsyncOperationTracker.GetJob(opId); if (job == null || job.Status != AsyncOperationTracker.JobStatus.Pending) { // Job already completed or doesn't exist, unsubscribe UnsubscribeIdleCallback(opId); return; } // Check timeout if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds)) { UnsubscribeIdleCallback(opId); AsyncOperationTracker.FailJob(opId, $"Idle wait timed out after {timeoutSeconds} seconds"); return; } // Check if editor is now idle bool isIdle = !EditorApplication.isCompiling && !EditorApplication.isUpdating; if (isIdle) { UnsubscribeIdleCallback(opId); var elapsed = (DateTime.UtcNow - job.CreatedAt).TotalSeconds; AsyncOperationTracker.CompleteJob(opId, "Editor is now idle.", new { isCompiling = false, isUpdating = false, elapsed = elapsed }); StateComposer.IncrementRevision(); } } private static void UnsubscribeIdleCallback(string opId) { EditorApplication.CallbackFunction callback; lock (_idleLock) { if (_idleCallbacks.TryGetValue(opId, out callback)) { EditorApplication.update -= callback; _idleCallbacks.Remove(opId); } } } // --- Helper Methods --- /// /// Gets the SerializedObject for the TagManager asset. /// private static SerializedObject GetTagManager() { try { // Load the TagManager asset from the ProjectSettings folder UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath( "ProjectSettings/TagManager.asset" ); if (tagManagerAssets == null || tagManagerAssets.Length == 0) { Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings."); return null; } // The first object in the asset file should be the TagManager return new SerializedObject(tagManagerAssets[0]); } catch (Exception e) { Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); return null; } } // --- Example Implementations for Settings --- /* private static object SetGameViewResolution(int width, int height) { ... } private static object SetQualityLevel(JToken qualityLevelToken) { ... } */ } // Helper class to get custom tool names (remains the same) internal static class EditorTools { public static string GetActiveToolName() { // This is a placeholder. Real implementation depends on how custom tools // are registered and tracked in the specific Unity project setup. // It might involve checking static variables, calling methods on specific tool managers, etc. if (UnityEditor.Tools.current == Tool.Custom) { // Example: Check a known custom tool manager // if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName; return "Unknown Custom Tool"; } return UnityEditor.Tools.current.ToString(); } } }