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