using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Codely.Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.PackageManager; using UnityEngine; using UnityTcp.Editor.Helpers; namespace UnityTcp.Editor.Tools { /// /// [EXPERIMENTAL] Handles baking operations (NavMesh, Lighting, etc.). /// Compatible with Unity 2022.3 LTS. /// public static class ManageBake { // Store callbacks for proper unsubscription private static readonly Dictionary _updateCallbacks = new Dictionary(); private static readonly object _callbackLock = new object(); // Store async operations for NavMesh baking private static readonly Dictionary> _navMeshBakeOperations = new Dictionary>(); // Runtime check for AI Navigation package availability private static bool? _hasAINavigation = null; private static Type _navMeshSurfaceType = null; private static MethodInfo _buildNavMeshMethod = null; private static MethodInfo _updateNavMeshMethod = null; private static PropertyInfo _activeSurfacesProperty = null; private static Type _navMeshType = null; private static MethodInfo _calculateTriangulationMethod = null; private static MethodInfo _removeAllNavMeshDataMethod = null; /// /// Reset the AI Navigation package cache. Call this after installing the package /// to force re-checking for available types. /// private static void ResetAINavigationCache() { _hasAINavigation = null; _navMeshSurfaceType = null; _buildNavMeshMethod = null; _updateNavMeshMethod = null; _activeSurfacesProperty = null; _navMeshType = null; _calculateTriangulationMethod = null; _removeAllNavMeshDataMethod = null; } private static bool HasAINavigation() { if (_hasAINavigation.HasValue) return _hasAINavigation.Value; try { // First, check if the package is installed via PackageManager bool packageInstalled = false; try { #if UNITY_2021_2_OR_NEWER // Use GetAllRegisteredPackages for Unity 2021.2+ var packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages(); packageInstalled = packages.Any(p => p.name == "com.unity.ai.navigation"); #else // Fallback for older Unity versions var listRequest = Client.List(true, false); while (!listRequest.IsCompleted) { System.Threading.Thread.Sleep(50); } if (listRequest.Status == StatusCode.Success) { packageInstalled = listRequest.Result.Any(p => p.name == "com.unity.ai.navigation"); } #endif } catch (Exception ex) { Debug.LogWarning($"[ManageBake] Error checking package installation: {ex.Message}"); // Continue with type checking as fallback } // Try to find NavMeshSurface type (Unity.AI.Navigation namespace from com.unity.ai.navigation package) // Try multiple methods to find the type _navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation"); if (_navMeshSurfaceType == null) { // Try with full assembly qualified name variations _navMeshSurfaceType = Type.GetType("Unity.AI.Navigation.NavMeshSurface, Unity.AI.Navigation, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); } if (_navMeshSurfaceType == null) { // Fallback: search in loaded assemblies by name first System.Reflection.Assembly targetAssembly = null; foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { var assemblyName = assembly.GetName().Name; if (assemblyName == "Unity.AI.Navigation" || assemblyName.Contains("Unity.AI.Navigation")) { targetAssembly = assembly; break; } } if (targetAssembly != null) { _navMeshSurfaceType = targetAssembly.GetType("Unity.AI.Navigation.NavMeshSurface"); } } if (_navMeshSurfaceType == null) { // Last resort: search all assemblies foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { _navMeshSurfaceType = assembly.GetType("Unity.AI.Navigation.NavMeshSurface"); if (_navMeshSurfaceType != null) break; } } if (_navMeshSurfaceType != null) { _buildNavMeshMethod = _navMeshSurfaceType.GetMethod("BuildNavMesh", BindingFlags.Public | BindingFlags.Instance); _updateNavMeshMethod = _navMeshSurfaceType.GetMethod("UpdateNavMesh", BindingFlags.Public | BindingFlags.Instance); _activeSurfacesProperty = _navMeshSurfaceType.GetProperty("activeSurfaces", BindingFlags.Public | BindingFlags.Static); } // Try to find NavMesh type (UnityEngine.AI namespace - still used by the package) _navMeshType = Type.GetType("UnityEngine.AI.NavMesh, UnityEngine.AIModule"); if (_navMeshType == null) { foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { _navMeshType = assembly.GetType("UnityEngine.AI.NavMesh"); if (_navMeshType != null) break; } } if (_navMeshType != null) { _calculateTriangulationMethod = _navMeshType.GetMethod("CalculateTriangulation", BindingFlags.Public | BindingFlags.Static); _removeAllNavMeshDataMethod = _navMeshType.GetMethod("RemoveAllNavMeshData", BindingFlags.Public | BindingFlags.Static); } // Check both package installation and required types/methods bool hasRequiredTypes = _navMeshSurfaceType != null && _buildNavMeshMethod != null && _navMeshType != null; // If package is installed but types are missing, check compilation status if (packageInstalled && !hasRequiredTypes) { bool isCompiling = EditorApplication.isCompiling; string compilationStatus = isCompiling ? "compiling" : "idle"; // Collect diagnostic information var loadedAssemblies = System.AppDomain.CurrentDomain.GetAssemblies() .Where(a => a.GetName().Name.Contains("AI") || a.GetName().Name.Contains("Navigation")) .Select(a => a.GetName().Name) .ToList(); string diagnosticInfo = ""; if (loadedAssemblies.Count > 0) { diagnosticInfo = $" Found related assemblies: {string.Join(", ", loadedAssemblies)}."; } else { diagnosticInfo = " No AI/Navigation assemblies found in loaded assemblies."; } string typeStatus = ""; if (_navMeshSurfaceType == null) { typeStatus += " NavMeshSurface type not found."; } else { typeStatus += $" NavMeshSurface found, but methods missing: BuildNavMesh={_buildNavMeshMethod != null}, UpdateNavMesh={_updateNavMeshMethod != null}, activeSurfaces={_activeSurfacesProperty != null}."; } if (_navMeshType == null) { typeStatus += " NavMesh type not found."; } Debug.LogWarning( $"[ManageBake] com.unity.ai.navigation package is installed but required types/methods are not available. " + $"Editor is currently {compilationStatus}.{diagnosticInfo}{typeStatus} " + (isCompiling ? "Please wait for compilation to complete, then call 'unity_editor { \"action\": \"wait_for_idle\" }' before retrying." : "The package may need to be reloaded. Try restarting Unity or wait a moment and retry.") ); } // Package installation check is primary, but we also need the types to be available // If package is installed but types are missing and we're not compiling, return false // If we're compiling, also return false (types won't be available until compilation completes) _hasAINavigation = packageInstalled && hasRequiredTypes && !EditorApplication.isCompiling; } catch (Exception ex) { Debug.LogWarning($"[ManageBake] Error checking for AI Navigation package: {ex.Message}"); _hasAINavigation = false; } return _hasAINavigation.Value; } public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } try { switch (action) { case "bake_navmesh": return BakeNavMesh(@params); case "bake_lighting": return BakeLighting(@params); case "wait_for_bake": return WaitForBake(@params); case "clear_navmesh": return ClearNavMesh(); case "clear_baked_data": return ClearBakedData(); default: return Response.Error( $"Unknown action: '{action}'. Valid actions: bake_navmesh, bake_lighting, wait_for_bake, clear_navmesh, clear_baked_data." ); } } catch (Exception e) { Debug.LogError($"[ManageBake] Action '{action}' failed: {e}"); return Response.Error($"[EXPERIMENTAL] Bake operation failed: {e.Message}"); } } private static object BakeNavMesh(JObject @params) { try { // Reset cache and re-check if first check fails (in case package was just installed) if (!HasAINavigation()) { ResetAINavigationCache(); if (!HasAINavigation()) { // Check if package is installed but types are not available bool packageInstalled = false; try { #if UNITY_2021_2_OR_NEWER var packages = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages(); packageInstalled = packages.Any(p => p.name == "com.unity.ai.navigation"); #else var listRequest = Client.List(true, false); while (!listRequest.IsCompleted) { System.Threading.Thread.Sleep(50); } if (listRequest.Status == StatusCode.Success) { packageInstalled = listRequest.Result.Any(p => p.name == "com.unity.ai.navigation"); } #endif } catch { } bool isCompiling = EditorApplication.isCompiling; string errorMessage; if (packageInstalled && isCompiling) { errorMessage = "[EXPERIMENTAL] NavMesh baking requires AI Navigation package types to be loaded. " + "The package is installed but Unity is currently compiling. " + "Please wait for compilation to complete by calling 'unity_editor { \"action\": \"wait_for_idle\" }', then retry."; } else if (packageInstalled) { errorMessage = "[EXPERIMENTAL] NavMesh baking requires AI Navigation package types to be loaded. " + "The package 'com.unity.ai.navigation' is installed but required types are not available. " + "This may happen if: (1) compilation is in progress, (2) the package needs to be reloaded, or (3) Unity needs to be restarted. " + "Try: (1) Call 'unity_editor { \"action\": \"wait_for_idle\" }' to ensure compilation is complete, " + "(2) Wait a few seconds and retry, or (3) Restart Unity."; } else { errorMessage = "[EXPERIMENTAL] NavMesh baking requires AI Navigation package. " + "Install 'com.unity.ai.navigation' via Package Manager using: " + "'unity_package { \"action\": \"install_package\", \"id_or_url\": \"com.unity.ai.navigation\" }', " + "then wait for installation and compilation to complete using 'unity_editor { \"action\": \"wait_for_idle\" }'."; } return Response.Error(errorMessage); } } var writeCheck = WriteGuard.CheckWriteAllowed("bake_navmesh"); if (writeCheck != null) return writeCheck; var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.NavMeshBake, "Baking NavMesh..." ); // Get all active NavMeshSurface components in the scene List surfaces = new List(); if (_activeSurfacesProperty != null) { var activeSurfaces = _activeSurfacesProperty.GetValue(null); if (activeSurfaces is System.Collections.IList surfaceList) { foreach (var surface in surfaceList) { surfaces.Add(surface); } } } if (surfaces.Count == 0) { // Fallback: find all NavMeshSurface components using Resources.FindObjectsOfTypeAll if (_navMeshSurfaceType != null) { var allObjects = Resources.FindObjectsOfTypeAll(_navMeshSurfaceType); foreach (var obj in allObjects) { if (obj != null) { surfaces.Add(obj); } } } } if (surfaces.Count == 0) { return Response.Error("[EXPERIMENTAL] No NavMeshSurface components found in the scene. Add a NavMeshSurface component to a GameObject to bake NavMesh."); } // Check if we should use async baking (UpdateNavMesh) or sync baking (BuildNavMesh) bool useAsync = @params["async"]?.ToObject() ?? false; List asyncOps = new List(); if (useAsync && _updateNavMeshMethod != null) { // Use async UpdateNavMesh for each surface that has existing data foreach (var surface in surfaces) { try { var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData"); if (navMeshDataProperty != null) { var navMeshData = navMeshDataProperty.GetValue(surface); if (navMeshData != null) { var asyncOp = _updateNavMeshMethod.Invoke(surface, new object[] { navMeshData }) as AsyncOperation; if (asyncOp != null) { asyncOps.Add(asyncOp); } } } } catch { // If UpdateNavMesh fails, fall back to BuildNavMesh } } } // If no async operations were started, use synchronous BuildNavMesh if (asyncOps.Count == 0) { foreach (var surface in surfaces) { _buildNavMeshMethod?.Invoke(surface, null); } // For synchronous baking, complete immediately bool hasNavMeshData = false; try { // First check NavMeshSurface components for navMeshData if (_navMeshSurfaceType != null) { var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData"); if (navMeshDataProperty != null) { foreach (var surface in surfaces) { if (surface != null) { var navMeshData = navMeshDataProperty.GetValue(surface); if (navMeshData != null) { hasNavMeshData = true; break; } } } } } // Fallback: check global NavMesh if NavMeshSurface check didn't find anything if (!hasNavMeshData && _calculateTriangulationMethod != null) { var triangulation = _calculateTriangulationMethod.Invoke(null, null); if (triangulation != null) { var verticesProperty = triangulation.GetType().GetProperty("vertices"); if (verticesProperty != null) { var vertices = verticesProperty.GetValue(triangulation) as Array; hasNavMeshData = vertices != null && vertices.Length > 0; } } } } catch { } AsyncOperationTracker.CompleteJob(job.OpId, "NavMesh baking completed", new { hasNavMeshData = hasNavMeshData, surfacesBaked = surfaces.Count }); StateComposer.IncrementRevision(); return AsyncOperationTracker.CreateCompleteResponse(job); } else { // Store async operations for tracking lock (_callbackLock) { _navMeshBakeOperations[job.OpId] = asyncOps; } // Create and store callback delegate for proper unsubscription EditorApplication.CallbackFunction callback = () => CheckNavMeshBake(job.OpId); lock (_callbackLock) { _updateCallbacks[job.OpId] = callback; } EditorApplication.update += callback; // Return standardized pending response var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary; response["poll_interval"] = 2.0; response["message"] = "[EXPERIMENTAL] NavMesh baking started (async)"; response["data"] = new { type = "navmesh", surfacesCount = surfaces.Count }; return response; } } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to start NavMesh baking: {e.Message}"); } } private static void CheckNavMeshBake(string opId) { if (!HasAINavigation()) return; try { // Check if async operations are still running List asyncOps = null; lock (_callbackLock) { if (_navMeshBakeOperations.TryGetValue(opId, out asyncOps)) { // Check if all operations are done bool allDone = asyncOps.All(op => op != null && op.isDone); if (allDone) { // Remove from tracking _navMeshBakeOperations.Remove(opId); // Properly unsubscribe using stored delegate if (_updateCallbacks.TryGetValue(opId, out var callback)) { EditorApplication.update -= callback; _updateCallbacks.Remove(opId); } // Check if NavMesh data exists using reflection bool hasNavMeshData = false; try { // First check NavMeshSurface components for navMeshData if (_navMeshSurfaceType != null) { var navMeshDataProperty = _navMeshSurfaceType.GetProperty("navMeshData"); if (navMeshDataProperty != null && _activeSurfacesProperty != null) { var activeSurfaces = _activeSurfacesProperty.GetValue(null); if (activeSurfaces is System.Collections.IList surfaceList) { foreach (var surface in surfaceList) { if (surface != null) { var navMeshData = navMeshDataProperty.GetValue(surface); if (navMeshData != null) { hasNavMeshData = true; break; } } } } } } // Fallback: check global NavMesh if NavMeshSurface check didn't find anything if (!hasNavMeshData && _calculateTriangulationMethod != null) { var triangulation = _calculateTriangulationMethod.Invoke(null, null); if (triangulation != null) { var verticesProperty = triangulation.GetType().GetProperty("vertices"); if (verticesProperty != null) { var vertices = verticesProperty.GetValue(triangulation) as Array; hasNavMeshData = vertices != null && vertices.Length > 0; } } } } catch { // If we can't check, assume no data hasNavMeshData = false; } AsyncOperationTracker.CompleteJob(opId, "NavMesh baking completed", new { hasNavMeshData = hasNavMeshData, surfacesBaked = asyncOps.Count }); StateComposer.IncrementRevision(); } } } } catch (Exception ex) { Debug.LogError($"[ManageBake] Error in CheckNavMeshBake: {ex.Message}"); } } private static object BakeLighting(JObject @params) { try { var writeCheck = WriteGuard.CheckWriteAllowed("bake_lighting"); if (writeCheck != null) return writeCheck; var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.LightingBake, "Baking lighting..." ); // Start async bake Lightmapping.BakeAsync(); // Create and store callback delegate for proper unsubscription EditorApplication.CallbackFunction callback = () => CheckLightingBake(job.OpId); lock (_callbackLock) { _updateCallbacks[job.OpId] = callback; } EditorApplication.update += callback; // Return standardized pending response var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary; response["poll_interval"] = 2.0; response["message"] = "[EXPERIMENTAL] Lighting baking started"; response["data"] = new { type = "lighting" }; return response; } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to start lighting baking: {e.Message}"); } } private static void CheckLightingBake(string opId) { if (!Lightmapping.isRunning) { // Properly unsubscribe using stored delegate EditorApplication.CallbackFunction callback; lock (_callbackLock) { if (_updateCallbacks.TryGetValue(opId, out callback)) { EditorApplication.update -= callback; _updateCallbacks.Remove(opId); } } AsyncOperationTracker.CompleteJob(opId, "Lighting baking completed", new { hasLightingData = Lightmapping.lightingDataAsset != null }); StateComposer.IncrementRevision(); } } private static object WaitForBake(JObject @params) { try { string opId = @params["op_id"]?.ToString(); int timeoutSeconds = @params["timeoutSeconds"]?.ToObject() ?? 600; if (string.IsNullOrEmpty(opId)) return Response.Error("'op_id' parameter required for wait_for_bake."); var job = AsyncOperationTracker.GetJob(opId); if (job == null) return Response.Error($"Operation {opId} not found."); if (job.Type != AsyncOperationTracker.JobType.NavMeshBake && job.Type != AsyncOperationTracker.JobType.LightingBake) return Response.Error($"Operation {opId} is not a bake operation."); if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds)) { AsyncOperationTracker.FailJob(opId, $"Bake operation timed out after {timeoutSeconds} seconds"); return AsyncOperationTracker.CreateErrorResponse(job); } switch (job.Status) { case AsyncOperationTracker.JobStatus.Complete: var response = AsyncOperationTracker.CreateCompleteResponse(job); return response; case AsyncOperationTracker.JobStatus.Error: return AsyncOperationTracker.CreateErrorResponse(job); case AsyncOperationTracker.JobStatus.Pending: return AsyncOperationTracker.CreatePendingResponse(job); default: return Response.Error($"Unknown job status: {job.Status}"); } } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to wait for bake: {e.Message}"); } } private static object ClearNavMesh() { try { if (!HasAINavigation()) { return Response.Error("[EXPERIMENTAL] NavMesh operations require AI Navigation package."); } var writeCheck = WriteGuard.CheckWriteAllowed("clear_navmesh"); if (writeCheck != null) return writeCheck; // Clear NavMesh using reflection - try RemoveAllNavMeshData first int clearedCount = 0; if (_removeAllNavMeshDataMethod != null) { _removeAllNavMeshDataMethod.Invoke(null, null); clearedCount++; } // Also clear all NavMeshSurface components if (_activeSurfacesProperty != null) { var activeSurfaces = _activeSurfacesProperty.GetValue(null); if (activeSurfaces is System.Collections.IList surfaceList) { var removeDataMethod = _navMeshSurfaceType.GetMethod("RemoveData", BindingFlags.Public | BindingFlags.Instance); foreach (var surface in surfaceList) { try { removeDataMethod?.Invoke(surface, null); clearedCount++; } catch { } } } } StateComposer.IncrementRevision(); return Response.Success($"[EXPERIMENTAL] NavMesh data cleared ({clearedCount} surfaces)."); } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to clear NavMesh: {e.Message}"); } } private static object ClearBakedData() { try { var writeCheck = WriteGuard.CheckWriteAllowed("clear_baked_data"); if (writeCheck != null) return writeCheck; Lightmapping.Clear(); StateComposer.IncrementRevision(); return Response.Success("[EXPERIMENTAL] Baked lighting data cleared."); } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to clear baked data: {e.Message}"); } } } }