1331 lines
57 KiB
C#
1331 lines
57 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 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.
|
||
/// </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;
|
||
|
||
// Compilation event tracking
|
||
private static bool _compilationCallbackRegistered = false;
|
||
private static readonly object _compilationLock = new object();
|
||
|
||
// Idle wait tracking
|
||
private static readonly Dictionary<string, EditorApplication.CallbackFunction> _idleCallbacks = new Dictionary<string, EditorApplication.CallbackFunction>();
|
||
private static readonly object _idleLock = new object();
|
||
|
||
/// <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.");
|
||
}
|
||
|
||
// 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<int?>() ?? 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<int?>() ?? 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?>();
|
||
// 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: 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 ---
|
||
|
||
/// <summary>
|
||
/// Returns the complete UnityCurrentState snapshot.
|
||
/// This is the primary entry point for LLMs to understand the full editor state.
|
||
/// </summary>
|
||
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 <Project>/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<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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Focuses an editor window by type name.
|
||
/// Supports common window names like "Console", "Inspector", "Hierarchy", "Project", "Scene", "Game".
|
||
/// </summary>
|
||
private static object FocusWindow(string windowType)
|
||
{
|
||
try
|
||
{
|
||
// Map common names to actual EditorWindow type names
|
||
var typeMap = new Dictionary<string, string>(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>();
|
||
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<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();
|
||
StateComposer.IncrementRevision();
|
||
return Response.Success($"Tag '{tagName}' added successfully.");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
return Response.Error($"Failed to add tag '{tagName}': {e.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Idempotent ensure tag - adds tag if not exists, returns success if already exists.
|
||
/// </summary>
|
||
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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Idempotent ensure layer - adds layer if not exists, returns success if already exists.
|
||
/// </summary>
|
||
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<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}");
|
||
}
|
||
}
|
||
|
||
// --- Compilation Management Methods ---
|
||
|
||
/// <summary>
|
||
/// Ensures compilation event callbacks are registered.
|
||
/// </summary>
|
||
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<string, object>
|
||
{
|
||
["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<string, object>;
|
||
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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["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<string, object>
|
||
{
|
||
["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<string, object>;
|
||
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<string, object> 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 ---
|
||
|
||
/// <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();
|
||
}
|
||
}
|
||
}
|
||
|