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
using UnityTcp.Editor.Tools.Jobs;
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();
// Compiling and play-mode transitions both reload the domain, so these budgets have to
// cover the reload itself — the step job's clock keeps running through it.
private const int DefaultCompileTimeoutSeconds = 300;
private const int DefaultPlayModeTimeoutSeconds = 120;
///
/// Starts on the current request and returns its context, which
/// tells the command loop to hold the response open. The job answers on this same request
/// once it reaches its end state — even if a domain reload happens in between, which is why
/// these are s and not coroutines.
///
private static object RunToCompletion(StepJob job, int timeoutSeconds)
=> StepJobRunner.Start(
CommandContext.RequestId, CommandContext.CommandType, job, timeoutSeconds);
private static readonly Dictionary> ActionHandlers =
new Dictionary>
{
{ "publish_dirty_state_if_needed", p => Deprecated(
p["action"]?.ToString(), "get_state") },
{ "wait_for_compile", WaitActionDeprecated },
{ "wait_for_stop", WaitActionDeprecated },
{ "request_compile", StartCompilation },
{ "start_compilation_pipeline", StartCompilation },
{ "refresh", Refresh },
{ "get_compilation_summary", _ => CompilationHelper.GetCompilationSummary() },
{ "wait_for_idle", p => StepJobRunner.Start(
CommandContext.RequestId, WaitForIdleJob.CommandName,
new WaitForIdleJob(),
p["timeoutSeconds"]?.ToObject() ?? 600) },
{ "play", p => RunToCompletion(new PlayJob(),
p["timeoutSeconds"]?.ToObject() ?? DefaultPlayModeTimeoutSeconds) },
{ "pause", Pause },
{ "resume", Resume },
{ "stop", p => RunToCompletion(new StopPlayModeJob(),
p["timeoutSeconds"]?.ToObject() ?? DefaultPlayModeTimeoutSeconds) },
{ "step", _ => StepFrame() },
{ "get_state", _ => GetEditorState() },
{ "get_current_state", _ => GetEditorState() },
{ "get_project_root", _ => GetProjectRoot() },
{ "get_windows", _ => GetEditorWindows() },
{ "get_active_tool", _ => GetActiveTool() },
{ "get_selection", _ => GetSelection() },
{ "drain_agent_input", _ =>
{
var pendingAgentInputs = Notifier.AgentInputNotifier.DrainPending();
return Response.Success(
$"Drained {pendingAgentInputs.Count} pending agent input message(s).",
pendingAgentInputs);
} },
{ "set_active_tool", p =>
{
string toolName = p["toolName"]?.ToString();
if (string.IsNullOrEmpty(toolName))
return Response.Error("'toolName' parameter required for set_active_tool.");
return SetActiveTool(toolName);
} },
{ "ensure_tag", RequireTag(EnsureTag, "ensure_tag") },
{ "add_tag", RequireTag(AddTag, "add_tag") },
{ "remove_tag", RequireTag(RemoveTag, "remove_tag") },
{ "get_tags", _ => GetTags() },
{ "ensure_layer", RequireLayer(EnsureLayer, "ensure_layer") },
{ "add_layer", RequireLayer(AddLayer, "add_layer") },
{ "remove_layer", RequireLayer(RemoveLayer, "remove_layer") },
{ "get_layers", _ => GetLayers() },
{ "focus_window", p =>
{
string windowType = p["windowType"]?.ToString();
if (string.IsNullOrEmpty(windowType))
return Response.Error("'windowType' parameter required for focus_window.");
return FocusWindow(windowType);
} },
};
public static object HandleCommand(JObject @params)
{
EnsureCompilationCallbacksRegistered();
return ActionRouter.Route(@params, ActionHandlers);
}
private static object WaitActionDeprecated(JObject @params)
{
string action = @params["action"]?.ToString()?.ToLower();
return Response.ErrorWithCode(
"action_deprecated",
$"Action '{action}' is deprecated. There is no need to wait: every " +
"command returns only after its work is done (compilation, play-mode " +
"transitions, imports included).");
}
private static object StartCompilation(JObject @params)
=> RunToCompletion(
new CompilePipelineJob
{
ClearCache = @params["clearCache"]?.ToObject() ?? false,
IncludeConsole =
(@params["include_console"] ?? @params["includeConsole"])
?.ToObject()
?? true,
},
@params["timeoutSeconds"]?.ToObject() ?? DefaultCompileTimeoutSeconds);
private static object Refresh(JObject @params)
=> RunToCompletion(
new RefreshJob(),
@params["timeoutSeconds"]?.ToObject() ?? DefaultCompileTimeoutSeconds);
private static object Pause(JObject @params)
{
var targetPaused = @params["targetPaused"];
if (targetPaused != null && targetPaused.Type != JTokenType.Boolean)
return Response.Error("'targetPaused' must be a boolean.");
return RunToCompletion(
new PauseJob
{
TogglePaused = targetPaused == null,
TargetPaused = targetPaused?.ToObject() ?? false,
},
@params["timeoutSeconds"]?.ToObject()
?? DefaultPlayModeTimeoutSeconds);
}
private static object Resume(JObject @params)
{
var singleFrame = @params["singleFrame"];
if (singleFrame == null)
return Response.Error("'singleFrame' parameter required for resume.");
if (singleFrame.Type != JTokenType.Boolean)
return Response.Error("'singleFrame' must be a boolean.");
return singleFrame.ToObject()
? StepFrame()
: RunToCompletion(
new PauseJob { TargetPaused = false },
@params["timeoutSeconds"]?.ToObject()
?? DefaultPlayModeTimeoutSeconds);
}
private static object StepFrame()
{
try
{
if (!EditorApplication.isPlaying)
{
Application.runInBackground = true;
EditorApplication.isPlaying = true;
EditorApplication.isPaused = true;
}
else if (!EditorApplication.isPaused)
{
EditorApplication.isPaused = true;
}
EditorApplication.Step();
return Response.Success("Stepped one frame.", new { playMode = "paused" });
}
catch (Exception e)
{
return Response.Error($"Error stepping frame: {e.Message}");
}
}
private static Func RequireTag(Func impl, string action)
=> p =>
{
string tagName = p["tagName"]?.ToString();
if (string.IsNullOrEmpty(tagName))
return Response.Error($"'tagName' parameter required for {action}.");
return impl(tagName);
};
private static Func RequireLayer(Func impl, string action)
=> p =>
{
string layerName = p["layerName"]?.ToString();
if (string.IsNullOrEmpty(layerName))
return Response.Error($"'layerName' parameter required for {action}.");
return impl(layerName);
};
private static object Deprecated(string action, string replacement)
=> Response.ErrorWithCode(
"action_deprecated",
$"Action '{action}' is deprecated. Use '{replacement}' instead.");
// --- 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