diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index 150055e17..eb41a8fb8 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using MCPForUnity.Editor.Helpers; // For Response class +using MCPForUnity.Runtime.Helpers; // For ScreenshotUtility using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; @@ -23,6 +24,8 @@ private sealed class SceneCommand public string name { get; set; } = string.Empty; public string path { get; set; } = string.Empty; public int? buildIndex { get; set; } + public string fileName { get; set; } = string.Empty; + public int? superSize { get; set; } } private static SceneCommand ToSceneCommand(JObject p) @@ -42,7 +45,9 @@ private static SceneCommand ToSceneCommand(JObject p) action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(), name = p["name"]?.ToString() ?? string.Empty, path = p["path"]?.ToString() ?? string.Empty, - buildIndex = BI(p["buildIndex"] ?? p["build_index"]) + buildIndex = BI(p["buildIndex"] ?? p["build_index"]), + fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty, + superSize = BI(p["superSize"] ?? p["super_size"] ?? p["supersize"]) }; } @@ -142,14 +147,26 @@ public static object HandleCommand(JObject @params) return ga; case "get_build_settings": return GetBuildSettingsScenes(); + case "screenshot": + return CaptureScreenshot(cmd.fileName, cmd.superSize); // Add cases for modifying build settings, additive loading, unloading etc. default: return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings." + $"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings, screenshot." ); } } + /// + /// Captures a screenshot to Assets/Screenshots and returns a response payload. + /// Public so the tools UI can reuse the same logic without duplicating parameters. + /// Available in both Edit Mode and Play Mode. + /// + public static object ExecuteScreenshot(string fileName = null, int? superSize = null) + { + return CaptureScreenshot(fileName, superSize); + } + private static object CreateScene(string fullPath, string relativePath) { if (File.Exists(fullPath)) @@ -329,6 +346,55 @@ private static object SaveScene(string fullPath, string relativePath) } } + private static object CaptureScreenshot(string fileName, int? superSize) + { + try + { + int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1; + ScreenshotCaptureResult result; + + if (Application.isPlaying) + { + result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true); + } + else + { + // Edit Mode path: render from the best-guess camera using RenderTexture. + Camera cam = Camera.main; + if (cam == null) + { + var cams = UnityEngine.Object.FindObjectsOfType(); + cam = cams.FirstOrDefault(); + } + + if (cam == null) + { + return new ErrorResponse("No camera found to capture screenshot in Edit Mode."); + } + + result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true); + } + + AssetDatabase.Refresh(); + + string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath})."; + + return new SuccessResponse( + message, + new + { + path = result.AssetsRelativePath, + fullPath = result.FullPath, + superSize = result.SuperSize, + } + ); + } + catch (Exception e) + { + return new ErrorResponse($"Error capturing screenshot: {e.Message}"); + } + } + private static object GetActiveSceneInfo() { try diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs index b6fc2b4a9..a19f3fd6b 100644 --- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs @@ -4,6 +4,7 @@ using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Services; +using MCPForUnity.Editor.Tools; using UnityEditor; using UnityEngine.UIElements; @@ -199,6 +200,11 @@ private VisualElement CreateToolRow(ToolMetadata tool) row.Add(parametersLabel); } + if (IsManageSceneTool(tool)) + { + row.Add(CreateManageSceneActions()); + } + return row; } @@ -258,6 +264,47 @@ private void AddInfoLabel(string message) categoryContainer?.Add(label); } + private VisualElement CreateManageSceneActions() + { + var actions = new VisualElement(); + actions.AddToClassList("tool-item-actions"); + + var screenshotButton = new Button(OnManageSceneScreenshotClicked) + { + text = "Capture Screenshot" + }; + screenshotButton.AddToClassList("tool-action-button"); + screenshotButton.style.marginTop = 4; + screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_scene."; + + actions.Add(screenshotButton); + return actions; + } + + private void OnManageSceneScreenshotClicked() + { + try + { + var response = ManageScene.ExecuteScreenshot(); + if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message)) + { + McpLog.Info(success.Message); + } + else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error)) + { + McpLog.Error(error.Error); + } + else + { + McpLog.Info("Screenshot capture requested."); + } + } + catch (Exception ex) + { + McpLog.Error($"Failed to capture screenshot: {ex.Message}"); + } + } + private static Label CreateTag(string text) { var tag = new Label(text); @@ -265,6 +312,8 @@ private static Label CreateTag(string text) return tag; } + private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase); + private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false; } } diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index 1a81947e4..d1932b191 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1,363 +1,363 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using MCPForUnity.Editor.Windows.Components.ClientConfig; -using MCPForUnity.Editor.Windows.Components.Connection; -using MCPForUnity.Editor.Windows.Components.Settings; -using UnityEditor; -using UnityEditor.UIElements; -using UnityEngine; -using UnityEngine.UIElements; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Windows.Components.Tools; - -namespace MCPForUnity.Editor.Windows -{ - public class MCPForUnityEditorWindow : EditorWindow - { - // Section controllers - private McpSettingsSection settingsSection; - private McpConnectionSection connectionSection; - private McpClientConfigSection clientConfigSection; - private McpToolsSection toolsSection; - - private ToolbarToggle settingsTabToggle; - private ToolbarToggle toolsTabToggle; - private VisualElement settingsPanel; - private VisualElement toolsPanel; - - private static readonly HashSet OpenWindows = new(); - private bool guiCreated = false; - private double lastRefreshTime = 0; - private const double RefreshDebounceSeconds = 0.5; - - private enum ActivePanel - { - Settings, - Tools - } - - internal static void CloseAllWindows() - { - var windows = OpenWindows.Where(window => window != null).ToArray(); - foreach (var window in windows) - { - window.Close(); - } - } - - public static void ShowWindow() - { - var window = GetWindow("MCP For Unity"); - window.minSize = new Vector2(500, 600); - } - - // Helper to check and manage open windows from other classes - public static bool HasAnyOpenWindow() - { - return OpenWindows.Count > 0; - } - - public static void CloseAllOpenWindows() - { - if (OpenWindows.Count == 0) - return; - - // Copy to array to avoid modifying the collection while iterating - var arr = new MCPForUnityEditorWindow[OpenWindows.Count]; - OpenWindows.CopyTo(arr); - foreach (var window in arr) - { - try - { - window?.Close(); - } - catch (Exception ex) - { - McpLog.Warn($"Error closing MCP window: {ex.Message}"); - } - } - } - - public void CreateGUI() - { - // Guard against repeated CreateGUI calls (e.g., domain reloads) - if (guiCreated) - return; - - string basePath = AssetPathUtility.GetMcpPackageRootPath(); - - // Load main window UXML - var visualTree = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" - ); - - if (visualTree == null) - { - McpLog.Error( - $"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" - ); - return; - } - - visualTree.CloneTree(rootVisualElement); - - // Load main window USS - var mainStyleSheet = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss" - ); - if (mainStyleSheet != null) - { - rootVisualElement.styleSheets.Add(mainStyleSheet); - } - - // Load common USS - var commonStyleSheet = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/Components/Common.uss" - ); - if (commonStyleSheet != null) - { - rootVisualElement.styleSheets.Add(commonStyleSheet); - } - - settingsPanel = rootVisualElement.Q("settings-panel"); - toolsPanel = rootVisualElement.Q("tools-panel"); - var settingsContainer = rootVisualElement.Q("settings-container"); - var toolsContainer = rootVisualElement.Q("tools-container"); - - if (settingsPanel == null || toolsPanel == null) - { - McpLog.Error("Failed to find tab panels in UXML"); - return; - } - - if (settingsContainer == null) - { - McpLog.Error("Failed to find settings-container in UXML"); - return; - } - - if (toolsContainer == null) - { - McpLog.Error("Failed to find tools-container in UXML"); - return; - } - - SetupTabs(); - - // Load and initialize Settings section - var settingsTree = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml" - ); - if (settingsTree != null) - { - var settingsRoot = settingsTree.Instantiate(); - settingsContainer.Add(settingsRoot); - settingsSection = new McpSettingsSection(settingsRoot); - settingsSection.OnGitUrlChanged += () => - clientConfigSection?.UpdateManualConfiguration(); - settingsSection.OnHttpServerCommandUpdateRequested += () => - connectionSection?.UpdateHttpServerCommandDisplay(); - } - - // Load and initialize Connection section - var connectionTree = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/Components/Connection/McpConnectionSection.uxml" - ); - if (connectionTree != null) - { - var connectionRoot = connectionTree.Instantiate(); - settingsContainer.Add(connectionRoot); - connectionSection = new McpConnectionSection(connectionRoot); - connectionSection.OnManualConfigUpdateRequested += () => - clientConfigSection?.UpdateManualConfiguration(); - } - - // Load and initialize Client Configuration section - var clientConfigTree = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml" - ); - if (clientConfigTree != null) - { - var clientConfigRoot = clientConfigTree.Instantiate(); - settingsContainer.Add(clientConfigRoot); - clientConfigSection = new McpClientConfigSection(clientConfigRoot); - } - - // Load and initialize Tools section - var toolsTree = AssetDatabase.LoadAssetAtPath( - $"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml" - ); - if (toolsTree != null) - { - var toolsRoot = toolsTree.Instantiate(); - toolsContainer.Add(toolsRoot); - toolsSection = new McpToolsSection(toolsRoot); - toolsSection.Refresh(); - } - else - { - McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable."); - } - guiCreated = true; - - // Initial updates - RefreshAllData(); - } - - private void OnEnable() - { - EditorApplication.update += OnEditorUpdate; - OpenWindows.Add(this); - } - - private void OnDisable() - { - EditorApplication.update -= OnEditorUpdate; - OpenWindows.Remove(this); - guiCreated = false; - } - - private void OnFocus() - { - // Only refresh data if UI is built - if (rootVisualElement == null || rootVisualElement.childCount == 0) - return; - - RefreshAllData(); - } - - private void OnEditorUpdate() - { - if (rootVisualElement == null || rootVisualElement.childCount == 0) - return; - - connectionSection?.UpdateConnectionStatus(); - } - - private void RefreshAllData() - { - // Debounce rapid successive calls (e.g., from OnFocus being called multiple times) - double currentTime = EditorApplication.timeSinceStartup; - if (currentTime - lastRefreshTime < RefreshDebounceSeconds) - { - return; - } - lastRefreshTime = currentTime; - - connectionSection?.UpdateConnectionStatus(); - - if (MCPServiceLocator.Bridge.IsRunning) - { - _ = connectionSection?.VerifyBridgeConnectionAsync(); - } - - settingsSection?.UpdatePathOverrides(); - clientConfigSection?.RefreshSelectedClient(); - } - - private void SetupTabs() - { - settingsTabToggle = rootVisualElement.Q("settings-tab"); - toolsTabToggle = rootVisualElement.Q("tools-tab"); - - settingsPanel?.RemoveFromClassList("hidden"); - toolsPanel?.RemoveFromClassList("hidden"); - - if (settingsTabToggle != null) - { - settingsTabToggle.RegisterValueChangedCallback(evt => - { - if (!evt.newValue) - { - if (toolsTabToggle != null && !toolsTabToggle.value) - { - settingsTabToggle.SetValueWithoutNotify(true); - } - return; - } - - SwitchPanel(ActivePanel.Settings); - }); - } - - if (toolsTabToggle != null) - { - toolsTabToggle.RegisterValueChangedCallback(evt => - { - if (!evt.newValue) - { - if (settingsTabToggle != null && !settingsTabToggle.value) - { - toolsTabToggle.SetValueWithoutNotify(true); - } - return; - } - - SwitchPanel(ActivePanel.Tools); - }); - } - - var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Settings.ToString()); - if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel)) - { - initialPanel = ActivePanel.Settings; - } - - SwitchPanel(initialPanel); - } - - private void SwitchPanel(ActivePanel panel) - { - bool showSettings = panel == ActivePanel.Settings; - - if (settingsPanel != null) - { - settingsPanel.style.display = showSettings ? DisplayStyle.Flex : DisplayStyle.None; - } - - if (toolsPanel != null) - { - toolsPanel.style.display = showSettings ? DisplayStyle.None : DisplayStyle.Flex; - } - - settingsTabToggle?.SetValueWithoutNotify(showSettings); - toolsTabToggle?.SetValueWithoutNotify(!showSettings); - - EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString()); - } - - internal static void RequestHealthVerification() - { - foreach (var window in OpenWindows) - { - window?.ScheduleHealthCheck(); - } - } - - private void ScheduleHealthCheck() - { - EditorApplication.delayCall += async () => - { - // Ensure window and components are still valid before execution - if (this == null || connectionSection == null) - { - return; - } - - try - { - await connectionSection.VerifyBridgeConnectionAsync(); - } - catch (Exception ex) - { - // Log but don't crash if verification fails during cleanup - McpLog.Warn($"Health check verification failed: {ex.Message}"); - } - }; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; +using MCPForUnity.Editor.Windows.Components.ClientConfig; +using MCPForUnity.Editor.Windows.Components.Connection; +using MCPForUnity.Editor.Windows.Components.Settings; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Windows.Components.Tools; + +namespace MCPForUnity.Editor.Windows +{ + public class MCPForUnityEditorWindow : EditorWindow + { + // Section controllers + private McpSettingsSection settingsSection; + private McpConnectionSection connectionSection; + private McpClientConfigSection clientConfigSection; + private McpToolsSection toolsSection; + + private ToolbarToggle settingsTabToggle; + private ToolbarToggle toolsTabToggle; + private VisualElement settingsPanel; + private VisualElement toolsPanel; + + private static readonly HashSet OpenWindows = new(); + private bool guiCreated = false; + private double lastRefreshTime = 0; + private const double RefreshDebounceSeconds = 0.5; + + private enum ActivePanel + { + Settings, + Tools + } + + internal static void CloseAllWindows() + { + var windows = OpenWindows.Where(window => window != null).ToArray(); + foreach (var window in windows) + { + window.Close(); + } + } + + public static void ShowWindow() + { + var window = GetWindow("MCP For Unity"); + window.minSize = new Vector2(500, 600); + } + + // Helper to check and manage open windows from other classes + public static bool HasAnyOpenWindow() + { + return OpenWindows.Count > 0; + } + + public static void CloseAllOpenWindows() + { + if (OpenWindows.Count == 0) + return; + + // Copy to array to avoid modifying the collection while iterating + var arr = new MCPForUnityEditorWindow[OpenWindows.Count]; + OpenWindows.CopyTo(arr); + foreach (var window in arr) + { + try + { + window?.Close(); + } + catch (Exception ex) + { + McpLog.Warn($"Error closing MCP window: {ex.Message}"); + } + } + } + + public void CreateGUI() + { + // Guard against repeated CreateGUI calls (e.g., domain reloads) + if (guiCreated) + return; + + string basePath = AssetPathUtility.GetMcpPackageRootPath(); + + // Load main window UXML + var visualTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" + ); + + if (visualTree == null) + { + McpLog.Error( + $"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml" + ); + return; + } + + visualTree.CloneTree(rootVisualElement); + + // Load main window USS + var mainStyleSheet = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss" + ); + if (mainStyleSheet != null) + { + rootVisualElement.styleSheets.Add(mainStyleSheet); + } + + // Load common USS + var commonStyleSheet = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/Components/Common.uss" + ); + if (commonStyleSheet != null) + { + rootVisualElement.styleSheets.Add(commonStyleSheet); + } + + settingsPanel = rootVisualElement.Q("settings-panel"); + toolsPanel = rootVisualElement.Q("tools-panel"); + var settingsContainer = rootVisualElement.Q("settings-container"); + var toolsContainer = rootVisualElement.Q("tools-container"); + + if (settingsPanel == null || toolsPanel == null) + { + McpLog.Error("Failed to find tab panels in UXML"); + return; + } + + if (settingsContainer == null) + { + McpLog.Error("Failed to find settings-container in UXML"); + return; + } + + if (toolsContainer == null) + { + McpLog.Error("Failed to find tools-container in UXML"); + return; + } + + SetupTabs(); + + // Load and initialize Settings section + var settingsTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/Components/Settings/McpSettingsSection.uxml" + ); + if (settingsTree != null) + { + var settingsRoot = settingsTree.Instantiate(); + settingsContainer.Add(settingsRoot); + settingsSection = new McpSettingsSection(settingsRoot); + settingsSection.OnGitUrlChanged += () => + clientConfigSection?.UpdateManualConfiguration(); + settingsSection.OnHttpServerCommandUpdateRequested += () => + connectionSection?.UpdateHttpServerCommandDisplay(); + } + + // Load and initialize Connection section + var connectionTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/Components/Connection/McpConnectionSection.uxml" + ); + if (connectionTree != null) + { + var connectionRoot = connectionTree.Instantiate(); + settingsContainer.Add(connectionRoot); + connectionSection = new McpConnectionSection(connectionRoot); + connectionSection.OnManualConfigUpdateRequested += () => + clientConfigSection?.UpdateManualConfiguration(); + } + + // Load and initialize Client Configuration section + var clientConfigTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml" + ); + if (clientConfigTree != null) + { + var clientConfigRoot = clientConfigTree.Instantiate(); + settingsContainer.Add(clientConfigRoot); + clientConfigSection = new McpClientConfigSection(clientConfigRoot); + } + + // Load and initialize Tools section + var toolsTree = AssetDatabase.LoadAssetAtPath( + $"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml" + ); + if (toolsTree != null) + { + var toolsRoot = toolsTree.Instantiate(); + toolsContainer.Add(toolsRoot); + toolsSection = new McpToolsSection(toolsRoot); + toolsSection.Refresh(); + } + else + { + McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable."); + } + guiCreated = true; + + // Initial updates + RefreshAllData(); + } + + private void OnEnable() + { + EditorApplication.update += OnEditorUpdate; + OpenWindows.Add(this); + } + + private void OnDisable() + { + EditorApplication.update -= OnEditorUpdate; + OpenWindows.Remove(this); + guiCreated = false; + } + + private void OnFocus() + { + // Only refresh data if UI is built + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; + + RefreshAllData(); + } + + private void OnEditorUpdate() + { + if (rootVisualElement == null || rootVisualElement.childCount == 0) + return; + + connectionSection?.UpdateConnectionStatus(); + } + + private void RefreshAllData() + { + // Debounce rapid successive calls (e.g., from OnFocus being called multiple times) + double currentTime = EditorApplication.timeSinceStartup; + if (currentTime - lastRefreshTime < RefreshDebounceSeconds) + { + return; + } + lastRefreshTime = currentTime; + + connectionSection?.UpdateConnectionStatus(); + + if (MCPServiceLocator.Bridge.IsRunning) + { + _ = connectionSection?.VerifyBridgeConnectionAsync(); + } + + settingsSection?.UpdatePathOverrides(); + clientConfigSection?.RefreshSelectedClient(); + } + + private void SetupTabs() + { + settingsTabToggle = rootVisualElement.Q("settings-tab"); + toolsTabToggle = rootVisualElement.Q("tools-tab"); + + settingsPanel?.RemoveFromClassList("hidden"); + toolsPanel?.RemoveFromClassList("hidden"); + + if (settingsTabToggle != null) + { + settingsTabToggle.RegisterValueChangedCallback(evt => + { + if (!evt.newValue) + { + if (toolsTabToggle != null && !toolsTabToggle.value) + { + settingsTabToggle.SetValueWithoutNotify(true); + } + return; + } + + SwitchPanel(ActivePanel.Settings); + }); + } + + if (toolsTabToggle != null) + { + toolsTabToggle.RegisterValueChangedCallback(evt => + { + if (!evt.newValue) + { + if (settingsTabToggle != null && !settingsTabToggle.value) + { + toolsTabToggle.SetValueWithoutNotify(true); + } + return; + } + + SwitchPanel(ActivePanel.Tools); + }); + } + + var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Settings.ToString()); + if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel)) + { + initialPanel = ActivePanel.Settings; + } + + SwitchPanel(initialPanel); + } + + private void SwitchPanel(ActivePanel panel) + { + bool showSettings = panel == ActivePanel.Settings; + + if (settingsPanel != null) + { + settingsPanel.style.display = showSettings ? DisplayStyle.Flex : DisplayStyle.None; + } + + if (toolsPanel != null) + { + toolsPanel.style.display = showSettings ? DisplayStyle.None : DisplayStyle.Flex; + } + + settingsTabToggle?.SetValueWithoutNotify(showSettings); + toolsTabToggle?.SetValueWithoutNotify(!showSettings); + + EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString()); + } + + internal static void RequestHealthVerification() + { + foreach (var window in OpenWindows) + { + window?.ScheduleHealthCheck(); + } + } + + private void ScheduleHealthCheck() + { + EditorApplication.delayCall += async () => + { + // Ensure window and components are still valid before execution + if (this == null || connectionSection == null) + { + return; + } + + try + { + await connectionSection.VerifyBridgeConnectionAsync(); + } + catch (Exception ex) + { + // Log but don't crash if verification fails during cleanup + McpLog.Warn($"Health check verification failed: {ex.Message}"); + } + }; + } + } +} diff --git a/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs new file mode 100644 index 000000000..81cefa31d --- /dev/null +++ b/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs @@ -0,0 +1,181 @@ +using System; +using System.IO; +using System.Linq; +using UnityEngine; + +namespace MCPForUnity.Runtime.Helpers +//The reason for having another Runtime Utilities in additional to Editor Utilities is to avoid Editor-only dependencies in this runtime code. +{ + public readonly struct ScreenshotCaptureResult + { + public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize) + { + FullPath = fullPath; + AssetsRelativePath = assetsRelativePath; + SuperSize = superSize; + } + + public string FullPath { get; } + public string AssetsRelativePath { get; } + public int SuperSize { get; } + } + + public static class ScreenshotUtility + { + private const string ScreenshotsFolderName = "Screenshots"; + + public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = null, int superSize = 1, bool ensureUniqueFileName = true) + { + int size = Mathf.Max(1, superSize); + string resolvedName = BuildFileName(fileName); + string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName); + Directory.CreateDirectory(folder); + + string fullPath = Path.Combine(folder, resolvedName); + if (ensureUniqueFileName) + { + fullPath = EnsureUnique(fullPath); + } + + string normalizedFullPath = fullPath.Replace('\\', '/'); + + // Use only the file name to let Unity decide the final location (per CaptureScreenshot docs). + string captureName = Path.GetFileName(normalizedFullPath); + ScreenCapture.CaptureScreenshot(captureName, size); + + Debug.Log($"Screenshot requested: file='{captureName}' intendedFullPath='{normalizedFullPath}' persistentDataPath='{Application.persistentDataPath}'"); + + string projectRoot = GetProjectRootPath(); + string assetsRelativePath = normalizedFullPath; + if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) + { + assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/'); + } + + return new ScreenshotCaptureResult( + normalizedFullPath, + assetsRelativePath, + size); + } + + /// + /// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode). + /// + public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera camera, string fileName = null, int superSize = 1, bool ensureUniqueFileName = true) + { + if (camera == null) + { + throw new ArgumentNullException(nameof(camera)); + } + + int size = Mathf.Max(1, superSize); + string resolvedName = BuildFileName(fileName); + string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName); + Directory.CreateDirectory(folder); + + string fullPath = Path.Combine(folder, resolvedName); + if (ensureUniqueFileName) + { + fullPath = EnsureUnique(fullPath); + } + + string normalizedFullPath = fullPath.Replace('\\', '/'); + + int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width); + int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height); + width *= size; + height *= size; + + RenderTexture prevRT = camera.targetTexture; + RenderTexture prevActive = RenderTexture.active; + var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32); + try + { + camera.targetTexture = rt; + camera.Render(); + + RenderTexture.active = rt; + var tex = new Texture2D(width, height, TextureFormat.RGBA32, false); + tex.ReadPixels(new Rect(0, 0, width, height), 0, 0); + tex.Apply(); + + byte[] png = tex.EncodeToPNG(); + File.WriteAllBytes(normalizedFullPath, png); + } + finally + { + camera.targetTexture = prevRT; + RenderTexture.active = prevActive; + RenderTexture.ReleaseTemporary(rt); + } + + string projectRoot = GetProjectRootPath(); + string assetsRelativePath = normalizedFullPath; + if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) + { + assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/'); + } + + return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size); + } + + private static string BuildFileName(string fileName) + { + string name = string.IsNullOrWhiteSpace(fileName) + ? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}" + : fileName.Trim(); + + name = SanitizeFileName(name); + + if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && + !name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) && + !name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase)) + { + name += ".png"; + } + + return name; + } + + private static string SanitizeFileName(string fileName) + { + var invalidChars = Path.GetInvalidFileNameChars(); + string cleaned = new string(fileName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray()); + + return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned; + } + + private static string EnsureUnique(string path) + { + if (!File.Exists(path)) + { + return path; + } + + string directory = Path.GetDirectoryName(path) ?? string.Empty; + string baseName = Path.GetFileNameWithoutExtension(path); + string extension = Path.GetExtension(path); + int counter = 1; + + string candidate; + do + { + candidate = Path.Combine(directory, $"{baseName}-{counter}{extension}"); + counter++; + } while (File.Exists(candidate)); + + return candidate; + } + + private static string GetProjectRootPath() + { + string root = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + root = root.Replace('\\', '/'); + if (!root.EndsWith("/", StringComparison.Ordinal)) + { + root += "/"; + } + return root; + } + } +} diff --git a/Server/src/services/tools/manage_scene.py b/Server/src/services/tools/manage_scene.py index 59d6ae9c1..5cb164a88 100644 --- a/Server/src/services/tools/manage_scene.py +++ b/Server/src/services/tools/manage_scene.py @@ -12,11 +12,21 @@ ) async def manage_scene( ctx: Context, - action: Annotated[Literal["create", "load", "save", "get_hierarchy", "get_active", "get_build_settings"], "Perform CRUD operations on Unity scenes."], + action: Annotated[Literal[ + "create", + "load", + "save", + "get_hierarchy", + "get_active", + "get_build_settings", + "screenshot", + ], "Perform CRUD operations on Unity scenes, and capture a screenshot."], name: Annotated[str, "Scene name."] | None = None, path: Annotated[str, "Scene path."] | None = None, build_index: Annotated[int | str, "Unity build index (quote as string, e.g., '0')."] | None = None, + screenshot_file_name: Annotated[str, "Screenshot file name (optional). Defaults to timestamp when omitted."] | None = None, + screenshot_super_size: Annotated[int | str, "Screenshot supersize multiplier (integer ≥1). Optional." ] | None = None, ) -> dict[str, Any]: # Get active instance from session state # Removed session_state import @@ -39,14 +49,19 @@ def _coerce_int(value, default=None): return default coerced_build_index = _coerce_int(build_index, default=None) + coerced_super_size = _coerce_int(screenshot_super_size, default=None) - params = {"action": action} + params: dict[str, Any] = {"action": action} if name: params["name"] = name if path: params["path"] = path if coerced_build_index is not None: params["buildIndex"] = coerced_build_index + if screenshot_file_name: + params["fileName"] = screenshot_file_name + if coerced_super_size is not None: + params["superSize"] = coerced_super_size # Use centralized retry helper with instance routing response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_scene", params) diff --git a/deploy-dev.bat b/deploy-dev.bat index 866ae2135..a99115bba 100644 --- a/deploy-dev.bat +++ b/deploy-dev.bat @@ -28,12 +28,8 @@ if "%PACKAGE_CACHE_PATH%"=="" ( exit /b 1 ) -:: Server installation path (with default) -echo. -echo Server Installation Path: -echo Default: %DEFAULT_SERVER_PATH% -set /p "SERVER_PATH=Enter server path (or press Enter for default): " -if "%SERVER_PATH%"=="" set "SERVER_PATH=%DEFAULT_SERVER_PATH%" +rem Server installation path prompt disabled (server deploy skipped) +set "SERVER_PATH=" :: Backup location (with default) echo. @@ -54,24 +50,12 @@ if not exist "%BRIDGE_SOURCE%" ( exit /b 1 ) -if not exist "%SERVER_SOURCE%" ( - echo Error: Server source not found: %SERVER_SOURCE% - pause - exit /b 1 -) - if not exist "%PACKAGE_CACHE_PATH%" ( echo Error: Package cache path not found: %PACKAGE_CACHE_PATH% pause exit /b 1 ) -if not exist "%SERVER_PATH%" ( - echo Error: Server installation path not found: %SERVER_PATH% - pause - exit /b 1 -) - :: Create backup directory if not exist "%BACKUP_DIR%" ( echo Creating backup directory: %BACKUP_DIR% @@ -103,16 +87,27 @@ if exist "%PACKAGE_CACHE_PATH%\Editor" ( ) ) -if exist "%SERVER_PATH%" ( - echo Backing up Python Server files... - xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul +if exist "%PACKAGE_CACHE_PATH%\Runtime" ( + echo Backing up Unity Runtime files... + xcopy "%PACKAGE_CACHE_PATH%\Runtime" "%BACKUP_SUBDIR%\UnityBridge\Runtime\" /E /I /Y > nul if !errorlevel! neq 0 ( - echo Error: Failed to backup Python Server files + echo Error: Failed to backup Unity Runtime files pause exit /b 1 ) ) +rem Server backup skipped (deprecated legacy deploy) +rem if exist "%SERVER_PATH%" ( +rem echo Backing up Python Server files... +rem xcopy "%SERVER_PATH%\*" "%BACKUP_SUBDIR%\PythonServer\" /E /I /Y > nul +rem if !errorlevel! neq 0 ( +rem echo Error: Failed to backup Python Server files +rem pause +rem exit /b 1 +rem ) +rem ) + :: Deploy Unity Bridge echo. echo Deploying Unity Bridge code... @@ -123,15 +118,23 @@ if !errorlevel! neq 0 ( exit /b 1 ) -:: Deploy Python Server -echo Deploying Python Server code... -xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul +echo Deploying Unity Runtime code... +xcopy "%BRIDGE_SOURCE%\Runtime\*" "%PACKAGE_CACHE_PATH%\Runtime\" /E /Y > nul if !errorlevel! neq 0 ( - echo Error: Failed to deploy Python Server code + echo Error: Failed to deploy Unity Runtime code pause exit /b 1 ) +rem Deploy Python Server (disabled; server no longer deployed this way) +rem echo Deploying Python Server code... +rem xcopy "%SERVER_SOURCE%\*" "%SERVER_PATH%\" /E /Y > nul +rem if !errorlevel! neq 0 ( +rem echo Error: Failed to deploy Python Server code +rem pause +rem exit /b 1 +rem ) + :: Success echo. echo ===============================================