Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handl
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
```

### Unity API Compatibility Shims
We support a wide Unity version range (2021+ → 6.x → CoreCLR 6.8). When an API is renamed, deprecated, or removed across versions, **don't sprinkle `#if UNITY_x_y_OR_NEWER` at every call site** — add a shim in `MCPForUnity/Runtime/Helpers/Unity*Compat.cs` and route every caller through it.

The catalog of active shims, the policy for when to add one, what does NOT belong in a shim, and the reflection-cache pattern all live in **`MCPForUnity/Runtime/Helpers/UnityCompatShims.cs`** — the XML doc on that empty marker class is the source of truth and ships inside the UPM package, so end-users can `F12`/Go-to-definition into it. Sources for current deprecations: Unity 6.x upgrade guides and the [CoreCLR 2026 thread](https://discussions.unity.com/t/path-to-coreclr-2026-upgrade-guide/1714279).

## Commands

### Running Tests
Expand Down
22 changes: 13 additions & 9 deletions MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
internal static class EditorWindowScreenshotUtility
{
private const string ScreenshotsFolderName = "Screenshots";
// Keep capture synchronous so callers can immediately return the screenshot payload.
// The short sleep gives Unity a chance to flush repaint work before GrabPixels reads the viewport.
private const int RepaintSettlingDelayMs = 75;
Expand All @@ -40,15 +39,16 @@ internal static class EditorWindowScreenshotUtility
/// <param name="maxResolution">Maximum edge length for the inline image payload.</param>
/// <param name="viewportWidth">Captured viewport width in pixels.</param>
/// <param name="viewportHeight">Captured viewport height in pixels.</param>
public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
public static ScreenshotCaptureResult CaptureSceneViewViewportToProject(
SceneView sceneView,
string fileName,
int superSize,
bool ensureUniqueFileName,
bool includeImage,
int maxResolution,
out int viewportWidth,
out int viewportHeight)
out int viewportHeight,
string folderOverride = null)
{
if (sceneView == null)
throw new ArgumentNullException(nameof(sceneView));
Expand All @@ -70,7 +70,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
{
captured = CaptureViewRect(sceneView, viewportRectPixels);

var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName);
var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName, folderOverride);
byte[] png = captured.EncodeToPNG();
File.WriteAllBytes(result.FullPath, png);

Expand All @@ -97,7 +97,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(

return new ScreenshotCaptureResult(
result.FullPath,
result.AssetsRelativePath,
result.ProjectRelativePath,
result.SuperSize,
false,
imageBase64,
Expand Down Expand Up @@ -317,11 +317,11 @@ private static void FlipTextureVertically(Texture2D texture)
texture.Apply();
}

private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName)
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride)
{
int size = Mathf.Max(1, superSize);
string resolvedName = BuildFileName(fileName);
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
string folder = ScreenshotUtility.ResolveFolderAbsolute(folderOverride);
Directory.CreateDirectory(folder);

string fullPath = Path.Combine(folder, resolvedName);
Expand All @@ -331,8 +331,12 @@ private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int
}

string normalizedFullPath = fullPath.Replace('\\', '/');
string assetsRelativePath = "Assets/" + normalizedFullPath.Substring(Application.dataPath.Length).TrimStart('/');
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, false);
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")).Replace('\\', '/');
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
string projectRelativePath = normalizedFullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase)
? normalizedFullPath.Substring(normalizedRoot.Length)
: normalizedFullPath;
return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, false);
}

private static string BuildFileName(string fileName)
Expand Down
49 changes: 49 additions & 0 deletions MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using MCPForUnity.Runtime.Helpers;
using UnityEditor;

namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Per-user EditorPrefs override for the default screenshot output folder.
/// Resolution priority used by callers:
/// 1. Per-call <c>output_folder</c> tool parameter
/// 2. <see cref="DefaultFolder"/> (this preference)
/// 3. <see cref="ScreenshotUtility.DefaultFolder"/> built-in fallback
/// </summary>
public static class ScreenshotPreferences
{
public const string EditorPrefsKey = "MCPForUnity_ScreenshotsFolder";

/// <summary>
/// User-configured default folder, or empty string when unset.
/// Stored as a project-relative path (e.g. "Assets/Screenshots", "Captures").
/// </summary>
public static string DefaultFolder
{
get => EditorPrefs.GetString(EditorPrefsKey, string.Empty);
set
{
if (string.IsNullOrWhiteSpace(value))
{
EditorPrefs.DeleteKey(EditorPrefsKey);
}
else
{
EditorPrefs.SetString(EditorPrefsKey, value.Trim());
}
Comment on lines +24 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the folder override before persisting it.

This setter currently stores any nonblank string. Because the new Advanced Settings field is editable, a typo or out-of-project absolute path becomes a sticky EditorPrefs value that makes later screenshot calls fail until the user clears it.

Suggested fix
         public static string DefaultFolder
         {
             get => EditorPrefs.GetString(EditorPrefsKey, string.Empty);
             set
             {
                 if (string.IsNullOrWhiteSpace(value))
                 {
                     EditorPrefs.DeleteKey(EditorPrefsKey);
                 }
                 else
                 {
-                    EditorPrefs.SetString(EditorPrefsKey, value.Trim());
+                    string trimmed = value.Trim();
+                    ScreenshotUtility.ResolveFolderAbsolute(trimmed);
+                    EditorPrefs.SetString(EditorPrefsKey, trimmed);
                 }
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set
{
if (string.IsNullOrWhiteSpace(value))
{
EditorPrefs.DeleteKey(EditorPrefsKey);
}
else
{
EditorPrefs.SetString(EditorPrefsKey, value.Trim());
}
set
{
if (string.IsNullOrWhiteSpace(value))
{
EditorPrefs.DeleteKey(EditorPrefsKey);
}
else
{
string trimmed = value.Trim();
ScreenshotUtility.ResolveFolderAbsolute(trimmed);
EditorPrefs.SetString(EditorPrefsKey, trimmed);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs` around lines 24 - 33, In
the setter that currently trims and writes to EditorPrefs (using EditorPrefsKey
and EditorPrefs.SetString/DeleteKey), validate the incoming value before
persisting: trim the value, treat empty/whitespace the same (call
EditorPrefs.DeleteKey), otherwise resolve the path and reject or convert invalid
absolute paths—if Path.IsPathRooted then check if the full path is inside the
Unity project (compare against Application.dataPath) and if so convert to a
project-relative path; if it points outside the project, do not call
EditorPrefs.SetString and surface a warning (e.g., Debug.LogWarning or an Editor
dialog); for relative paths ensure the directory exists (create with
Directory.CreateDirectory if needed) before calling
EditorPrefs.SetString(EditorPrefsKey, trimmedValue).

}
}

/// <summary>
/// Resolves the effective folder: caller override → user pref → built-in default.
/// Returns a project-relative path string suitable for <see cref="ScreenshotUtility.ResolveFolderAbsolute"/>.
/// </summary>
public static string Resolve(string callerOverride)
{
if (!string.IsNullOrWhiteSpace(callerOverride)) return callerOverride.Trim();
string pref = DefaultFolder;
if (!string.IsNullOrWhiteSpace(pref)) return pref;
return ScreenshotUtility.DefaultFolder;
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion MCPForUnity/Editor/Helpers/UnityTypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Runtime.Helpers;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
Expand Down Expand Up @@ -150,7 +151,7 @@ private static void Cache(Type t)
private static List<Type> FindCandidates(string query, Type requiredBaseType)
{
bool isShort = !query.Contains('.');
var loaded = AppDomain.CurrentDomain.GetAssemblies();
var loaded = UnityAssembliesCompat.GetLoadedAssemblies();

#if UNITY_EDITOR
// Names of Player (runtime) script assemblies
Expand Down
3 changes: 2 additions & 1 deletion MCPForUnity/Editor/Tools/Animation/ClipCreate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Runtime.Helpers;
using UnityEditor;
using UnityEngine;

Expand Down Expand Up @@ -491,7 +492,7 @@ private static Type ResolveType(string typeName)
if (type != null) return type;

// Fallback: search all loaded assemblies
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
{
type = assembly.GetType(typeName);
if (type != null) return type;
Expand Down
3 changes: 2 additions & 1 deletion MCPForUnity/Editor/Tools/CommandRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources;
using MCPForUnity.Runtime.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -59,7 +60,7 @@ private static void AutoDiscoverCommands()
{
try
{
var allTypes = AppDomain.CurrentDomain.GetAssemblies()
var allTypes = UnityAssembliesCompat.GetLoadedAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a =>
{
Expand Down
3 changes: 2 additions & 1 deletion MCPForUnity/Editor/Tools/ExecuteCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using System.Text;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Runtime.Helpers;
using Microsoft.CSharp;
using Newtonsoft.Json.Linq;
using UnityEngine;
Expand Down Expand Up @@ -360,7 +361,7 @@ private static string[] ResolveAssemblyPaths()
{
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
{
try
{
Expand Down
Loading