Files
daluandou/.codely.packages/cn.tuanjie.codely.bridge@1.0.85-785dea6a/Editor/Bridge/Tools/ManageEditor.cs
T
huadexuan 53d95802e5 ```
feat(gameplay): 更新单位属性和材质配置

- 调整多个单位的基础生命值和攻击力数值平衡
- 修改CoreBattleGround材质的纹理缩放比例
- 更新地砖材质颜色为白色
- 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持
- 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法
- 为水晶矿添加中立阵营设置和光球视觉效果同步
- 调整预制体中的全局缩放参数
- 为狼人和石巨人添加NavMeshAgent组件支持
- 优化水晶矿血条显示位置计算方式
```
2026-09-20 21:52:31 +08:00

1073 lines
44 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
using UnityTcp.Editor.Tools.Jobs;
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();
// 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;
/// <summary>
/// Starts <paramref name="job"/> 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 <see cref="StepJob"/>s and not coroutines.
/// </summary>
private static object RunToCompletion(StepJob job, int timeoutSeconds)
=> StepJobRunner.Start(
CommandContext.RequestId, CommandContext.CommandType, job, timeoutSeconds);
private static readonly Dictionary<string, Func<JObject, object>> ActionHandlers =
new Dictionary<string, Func<JObject, object>>
{
{ "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<int?>() ?? 600) },
{ "play", p => RunToCompletion(new PlayJob(),
p["timeoutSeconds"]?.ToObject<int?>() ?? DefaultPlayModeTimeoutSeconds) },
{ "pause", Pause },
{ "resume", Resume },
{ "stop", p => RunToCompletion(new StopPlayModeJob(),
p["timeoutSeconds"]?.ToObject<int?>() ?? 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<bool?>() ?? false,
IncludeConsole =
(@params["include_console"] ?? @params["includeConsole"])
?.ToObject<bool?>()
?? true,
},
@params["timeoutSeconds"]?.ToObject<int?>() ?? DefaultCompileTimeoutSeconds);
private static object Refresh(JObject @params)
=> RunToCompletion(
new RefreshJob(),
@params["timeoutSeconds"]?.ToObject<int?>() ?? 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<bool>() ?? false,
},
@params["timeoutSeconds"]?.ToObject<int?>()
?? 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<bool>()
? StepFrame()
: RunToCompletion(
new PauseJob { TargetPaused = false },
@params["timeoutSeconds"]?.ToObject<int?>()
?? 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<JObject, object> RequireTag(Func<string, object> 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<JObject, object> RequireLayer(Func<string, object> 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 <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.GetStableInstanceId(),
}
);
}
catch (Exception ex)
{
CodelyLogger.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."
);
}
}
// Bring the Unity application itself to the foreground first; otherwise
// EditorWindow.Focus() only changes focus *within* Unity and the user
// won't see it if another app or a minimized Unity is on top.
NativeWindowFocus.BringUnityToFront();
// 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.GetStableInstanceId()
}
);
}
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}");
}
}
/// <summary>
/// Returns the hierarchy path of a GameObject (e.g. "Root/Child/Grandchild").
/// Kept consistent with SelectionChangedNotifier so the `path` field has identical
/// semantics in get_selection responses and selection_changed notifications.
/// </summary>
private static string GetGameObjectPath(UnityEngine.GameObject obj)
{
if (obj == null) return string.Empty;
var path = obj.name;
var parent = obj.transform != null ? obj.transform.parent : null;
while (parent != null)
{
path = $"{parent.name}/{path}";
parent = parent.parent;
}
return path;
}
private static object GetSelection()
{
try
{
string activeAssetPath = Selection.activeObject != null
? AssetDatabase.GetAssetPath(Selection.activeObject)
: null;
string activeAsset = string.IsNullOrEmpty(activeAssetPath)
? null
: Path.GetFullPath(activeAssetPath).Replace('\\', '/');
var selectionInfo = new
{
activeObject = Selection.activeObject?.name,
activeAsset = activeAsset,
activeGameObject = Selection.activeGameObject?.name,
activeTransform = Selection.activeTransform?.name,
activeInstanceID = InstanceIdExtensions.ActiveSelectionInstanceId(),
#if UNITY_2020_1_OR_NEWER
count = Selection.count,
#else
count = Selection.objects?.Length ?? 0,
#endif
objects = Selection
.objects.Select(obj => new
{
name = obj?.name,
type = obj?.GetType().FullName,
instanceID = obj?.GetStableInstanceId(),
})
.ToList(),
gameObjects = Selection
.gameObjects.Select(go => new
{
name = go?.name,
path = GetGameObjectPath(go),
instanceID = go?.GetStableInstanceId(),
})
.ToList(),
assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view
assetPaths = Selection.assetGUIDs?.Select(AssetDatabase.GUIDToAssetPath).ToArray(),
};
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}");
}
}
/// <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();
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();
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();
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. These only log: compilation state is
/// read straight from the editor wherever it matters (<see cref="WaitForCompileJob"/>,
/// <see cref="StateComposer"/>), so there is no bookkeeping to keep in step.
/// </summary>
private static void EnsureCompilationCallbacksRegistered()
{
lock (_compilationLock)
{
if (!_compilationCallbackRegistered)
{
UnityEditor.Compilation.CompilationPipeline.compilationStarted += OnCompilationStarted;
UnityEditor.Compilation.CompilationPipeline.compilationFinished += OnCompilationFinished;
_compilationCallbackRegistered = true;
CodelyLogger.Log("[ManageEditor] Compilation callbacks registered");
}
}
}
private static void OnCompilationStarted(object obj)
=> CodelyLogger.Log("[ManageEditor] Compilation started");
private static void OnCompilationFinished(object obj)
=> CodelyLogger.Log("[ManageEditor] Compilation finished");
// --- 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)
{
CodelyLogger.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)
{
CodelyLogger.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}");
return null;
}
}
}
/// <summary>
/// Brings the Unity Editor application window to the OS foreground.
/// On Windows uses Win32 (SetForegroundWindow with the AttachThreadInput trick to
/// bypass the foreground-lock restriction). On macOS calls
/// [[NSApplication sharedApplication] activateIgnoringOtherApps:YES] via the Obj-C runtime.
/// </summary>
internal static class NativeWindowFocus
{
public static void BringUnityToFront()
{
#if UNITY_EDITOR_WIN
BringToFrontWin();
#elif UNITY_EDITOR_OSX
BringToFrontMac();
#endif
}
#if UNITY_EDITOR_WIN
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool IsIconic(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
private const int SW_RESTORE = 9;
private static void BringToFrontWin()
{
try
{
var hwnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
if (hwnd == IntPtr.Zero) return;
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
var foreHwnd = GetForegroundWindow();
uint foreThread = GetWindowThreadProcessId(foreHwnd, out _);
uint thisThread = GetCurrentThreadId();
bool attached = false;
if (foreThread != 0 && foreThread != thisThread)
{
attached = AttachThreadInput(foreThread, thisThread, true);
}
SetForegroundWindow(hwnd);
if (attached)
{
AttachThreadInput(foreThread, thisThread, false);
}
}
catch (Exception e)
{
CodelyLogger.LogWarning($"[NativeWindowFocus] Windows bring-to-front failed: {e.Message}");
}
}
#endif
#if UNITY_EDITOR_OSX
private const string LibObjC = "/usr/lib/libobjc.dylib";
[System.Runtime.InteropServices.DllImport(LibObjC)]
private static extern IntPtr objc_getClass(string name);
[System.Runtime.InteropServices.DllImport(LibObjC)]
private static extern IntPtr sel_registerName(string name);
[System.Runtime.InteropServices.DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector);
[System.Runtime.InteropServices.DllImport(LibObjC, EntryPoint = "objc_msgSend")]
private static extern void objc_msgSend_bool(IntPtr receiver, IntPtr selector, bool arg);
private static void BringToFrontMac()
{
try
{
// Equivalent of: [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
var nsAppClass = objc_getClass("NSApplication");
if (nsAppClass == IntPtr.Zero) return;
var sharedSel = sel_registerName("sharedApplication");
var nsApp = objc_msgSend(nsAppClass, sharedSel);
if (nsApp == IntPtr.Zero) return;
var activateSel = sel_registerName("activateIgnoringOtherApps:");
objc_msgSend_bool(nsApp, activateSel, true);
}
catch (Exception e)
{
CodelyLogger.LogWarning($"[NativeWindowFocus] macOS bring-to-front failed: {e.Message}");
}
}
#endif
}
// 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();
}
}
}