592 lines
24 KiB
C#
592 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEditor;
|
|
using UnityEditorInternal; // Required for tag management
|
|
using UnityEngine;
|
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
|
|
|
namespace UnityMcpBridge.Editor.Tools
|
|
{
|
|
/// <summary>
|
|
/// Handles operations related to controlling and querying the Unity Editor state,
|
|
/// including managing Tags and Layers.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Main handler for editor management actions.
|
|
/// </summary>
|
|
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<bool>() ?? 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_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 "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?>();
|
|
// int? height = @params["height"]?.ToObject<int?>();
|
|
// 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_windows, get_active_tool, get_selection, 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 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<object>();
|
|
|
|
// Find currently open instances
|
|
// Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows
|
|
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
|
|
|
|
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 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<Tool>(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<int, string>();
|
|
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 ---
|
|
|
|
/// <summary>
|
|
/// Gets the SerializedObject for the TagManager asset.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|