using System; using System.Collections.Generic; using System.Linq; using System.IO; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditorInternal; // Required for tag management using UnityEditor.SceneManagement; using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools { /// /// Handles operations related to controlling and querying the Unity Editor state, /// including managing Tags and Layers. /// [McpForUnityTool("manage_editor")] 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; /// /// 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."); } // Route action switch (action) { // Play Mode Control case "play": try { if (!EditorApplication.isPlaying) { EditorApplication.isPlaying = true; return Response.Success("Entered play mode."); } return Response.Success("Already in play mode."); } catch (Exception e) { return Response.Error($"Error entering play mode: {e.Message}"); } case "pause": try { if (EditorApplication.isPlaying) { EditorApplication.isPaused = !EditorApplication.isPaused; return Response.Success( EditorApplication.isPaused ? "Game paused." : "Game resumed." ); } 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."); } return Response.Success("Already stopped (not in play mode)."); } 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 "get_prefab_stage": return GetPrefabStageInfo(); 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 "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 "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 // --- 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 play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, get_prefab_stage, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers." ); } } // --- Editor State/Info Methods --- private static object GetEditorState() { try { var state = new { isPlaying = EditorApplication.isPlaying, isPaused = EditorApplication.isPaused, isCompiling = EditorApplication.isCompiling, isUpdating = EditorApplication.isUpdating, applicationPath = EditorApplication.applicationPath, applicationContentsPath = EditorApplication.applicationContentsPath, timeSinceStartup = EditorApplication.timeSinceStartup, }; return Response.Success("Retrieved editor state.", state); } 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}"); } } private static object GetPrefabStageInfo() { try { PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); if (stage == null) { return Response.Success ("No prefab stage is currently open.", new { isOpen = false }); } return Response.Success( "Prefab stage info retrieved.", new { isOpen = true, assetPath = stage.assetPath, prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null, mode = stage.mode.ToString(), isDirty = stage.scene.isDirty } ); } catch (Exception e) { return Response.Error($"Error getting prefab stage info: {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 var toolInfo = new { activeTool = activeToolName, isCustom = customToolActive, pivotMode = UnityEditor.Tools.pivotMode.ToString(), pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity handlePosition = UnityEditor.Tools.handlePosition, }; 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(); return Response.Success($"Tag '{tagName}' added successfully."); } catch (Exception e) { return Response.Error($"Failed to add 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(); return Response.Success( $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." ); } catch (Exception e) { return Response.Error($"Failed to add 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}"); } } // --- 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(); } } }