Skip to content

Commit a90ab06

Browse files
authored
Merge branch 'beta' into feature/game-view-uitoolkit-screenshot-capture
2 parents 7aa4315 + 9b0a662 commit a90ab06

39 files changed

Lines changed: 1300 additions & 186 deletions

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handl
140140
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
141141
```
142142

143+
### Unity API Compatibility Shims
144+
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.
145+
146+
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).
147+
143148
## Commands
144149

145150
### Running Tests

MCPForUnity/Editor/Helpers/EditorWindowScreenshotUtility.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ namespace MCPForUnity.Editor.Helpers
1616
/// </summary>
1717
internal static class EditorWindowScreenshotUtility
1818
{
19-
private const string ScreenshotsFolderName = "Screenshots";
2019
// Keep capture synchronous so callers can immediately return the screenshot payload.
2120
// The short sleep gives Unity a chance to flush repaint work before GrabPixels reads the viewport.
2221
private const int RepaintSettlingDelayMs = 75;
@@ -40,15 +39,16 @@ internal static class EditorWindowScreenshotUtility
4039
/// <param name="maxResolution">Maximum edge length for the inline image payload.</param>
4140
/// <param name="viewportWidth">Captured viewport width in pixels.</param>
4241
/// <param name="viewportHeight">Captured viewport height in pixels.</param>
43-
public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
42+
public static ScreenshotCaptureResult CaptureSceneViewViewportToProject(
4443
SceneView sceneView,
4544
string fileName,
4645
int superSize,
4746
bool ensureUniqueFileName,
4847
bool includeImage,
4948
int maxResolution,
5049
out int viewportWidth,
51-
out int viewportHeight)
50+
out int viewportHeight,
51+
string folderOverride = null)
5252
{
5353
if (sceneView == null)
5454
throw new ArgumentNullException(nameof(sceneView));
@@ -70,7 +70,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
7070
{
7171
captured = CaptureViewRect(sceneView, viewportRectPixels);
7272

73-
var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName);
73+
var result = PrepareCaptureResult(fileName, effectiveSuperSize, ensureUniqueFileName, folderOverride);
7474
byte[] png = captured.EncodeToPNG();
7575
File.WriteAllBytes(result.FullPath, png);
7676

@@ -97,7 +97,7 @@ public static ScreenshotCaptureResult CaptureSceneViewViewportToAssets(
9797

9898
return new ScreenshotCaptureResult(
9999
result.FullPath,
100-
result.AssetsRelativePath,
100+
result.ProjectRelativePath,
101101
result.SuperSize,
102102
false,
103103
imageBase64,
@@ -317,11 +317,11 @@ private static void FlipTextureVertically(Texture2D texture)
317317
texture.Apply();
318318
}
319319

320-
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName)
320+
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride)
321321
{
322322
int size = Mathf.Max(1, superSize);
323323
string resolvedName = BuildFileName(fileName);
324-
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
324+
string folder = ScreenshotUtility.ResolveFolderAbsolute(folderOverride);
325325
Directory.CreateDirectory(folder);
326326

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

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

338342
private static string BuildFileName(string fileName)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using MCPForUnity.Runtime.Helpers;
2+
using UnityEditor;
3+
4+
namespace MCPForUnity.Editor.Helpers
5+
{
6+
/// <summary>
7+
/// Per-user EditorPrefs override for the default screenshot output folder.
8+
/// Resolution priority used by callers:
9+
/// 1. Per-call <c>output_folder</c> tool parameter
10+
/// 2. <see cref="DefaultFolder"/> (this preference)
11+
/// 3. <see cref="ScreenshotUtility.DefaultFolder"/> built-in fallback
12+
/// </summary>
13+
public static class ScreenshotPreferences
14+
{
15+
public const string EditorPrefsKey = "MCPForUnity_ScreenshotsFolder";
16+
17+
/// <summary>
18+
/// User-configured default folder, or empty string when unset.
19+
/// Stored as a project-relative path (e.g. "Assets/Screenshots", "Captures").
20+
/// </summary>
21+
public static string DefaultFolder
22+
{
23+
get => EditorPrefs.GetString(EditorPrefsKey, string.Empty);
24+
set
25+
{
26+
if (string.IsNullOrWhiteSpace(value))
27+
{
28+
EditorPrefs.DeleteKey(EditorPrefsKey);
29+
}
30+
else
31+
{
32+
EditorPrefs.SetString(EditorPrefsKey, value.Trim());
33+
}
34+
}
35+
}
36+
37+
/// <summary>
38+
/// Resolves the effective folder: caller override → user pref → built-in default.
39+
/// Returns a project-relative path string suitable for <see cref="ScreenshotUtility.ResolveFolderAbsolute"/>.
40+
/// </summary>
41+
public static string Resolve(string callerOverride)
42+
{
43+
if (!string.IsNullOrWhiteSpace(callerOverride)) return callerOverride.Trim();
44+
string pref = DefaultFolder;
45+
if (!string.IsNullOrWhiteSpace(pref)) return pref;
46+
return ScreenshotUtility.DefaultFolder;
47+
}
48+
}
49+
}

MCPForUnity/Editor/Helpers/ScreenshotPreferences.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Helpers/UnityTypeResolver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5+
using MCPForUnity.Runtime.Helpers;
56
using UnityEngine;
67
#if UNITY_EDITOR
78
using UnityEditor;
@@ -150,7 +151,7 @@ private static void Cache(Type t)
150151
private static List<Type> FindCandidates(string query, Type requiredBaseType)
151152
{
152153
bool isShort = !query.Contains('.');
153-
var loaded = AppDomain.CurrentDomain.GetAssemblies();
154+
var loaded = UnityAssembliesCompat.GetLoadedAssemblies();
154155

155156
#if UNITY_EDITOR
156157
// Names of Player (runtime) script assemblies

MCPForUnity/Editor/Services/TestJobManager.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ internal sealed class TestJob
4040
public List<TestJobFailure> FailuresSoFar { get; set; }
4141
public string Error { get; set; }
4242
public TestRunResult Result { get; set; }
43+
public long InitTimeoutMs { get; set; }
4344
}
4445

4546
/// <summary>
@@ -50,7 +51,8 @@ internal static class TestJobManager
5051
// Keep this small to avoid ballooning payloads during polling.
5152
private const int FailureCap = 25;
5253
private const long StuckThresholdMs = 60_000;
53-
private const long InitializationTimeoutMs = 15_000; // 15 seconds to call OnRunStarted, else fail
54+
private const long DefaultInitializationTimeoutMs = 15_000; // 15 seconds default; override per-job via run_tests init_timeout param
55+
private const long MaxInitializationTimeoutMs = 600_000; // 10 minutes hard cap
5456
private const int MaxJobsToKeep = 10;
5557
private const long MinPersistIntervalMs = 1000; // Throttle persistence to reduce overhead
5658

@@ -139,6 +141,7 @@ private sealed class PersistedJob
139141
public long? last_finished_unix_ms { get; set; }
140142
public List<TestJobFailure> failures_so_far { get; set; }
141143
public string error { get; set; }
144+
public long init_timeout_ms { get; set; }
142145
}
143146

144147
private static TestJobStatus ParseStatus(string status)
@@ -201,6 +204,7 @@ private static void TryRestoreFromSessionState()
201204
LastFinishedUnixMs = pj.last_finished_unix_ms,
202205
FailuresSoFar = pj.failures_so_far ?? new List<TestJobFailure>(),
203206
Error = pj.error,
207+
InitTimeoutMs = pj.init_timeout_ms,
204208
// Intentionally not persisted to avoid ballooning SessionState.
205209
Result = null
206210
};
@@ -273,7 +277,8 @@ private static void PersistToSessionState(bool force = false)
273277
last_finished_test_full_name = j.LastFinishedTestFullName,
274278
last_finished_unix_ms = j.LastFinishedUnixMs,
275279
failures_so_far = (j.FailuresSoFar ?? new List<TestJobFailure>()).Take(FailureCap).ToList(),
276-
error = j.Error
280+
error = j.Error,
281+
init_timeout_ms = j.InitTimeoutMs
277282
})
278283
.ToList();
279284

@@ -294,8 +299,12 @@ private static void PersistToSessionState(bool force = false)
294299
}
295300
}
296301

297-
public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null)
302+
public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null, long initTimeoutMs = 0)
298303
{
304+
// Clamp to valid range: non-positive values mean "use default", cap at 10 minutes
305+
if (initTimeoutMs < 0) initTimeoutMs = 0;
306+
if (initTimeoutMs > MaxInitializationTimeoutMs) initTimeoutMs = MaxInitializationTimeoutMs;
307+
299308
string jobId = Guid.NewGuid().ToString("N");
300309
long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
301310
string modeStr = mode.ToString();
@@ -316,7 +325,8 @@ public static string StartJob(TestMode mode, TestFilterOptions filterOptions = n
316325
LastFinishedUnixMs = null,
317326
FailuresSoFar = new List<TestJobFailure>(),
318327
Error = null,
319-
Result = null
328+
Result = null,
329+
InitTimeoutMs = initTimeoutMs
320330
};
321331

322332
// Single lock scope for check-and-set to avoid TOCTOU race
@@ -491,9 +501,10 @@ internal static TestJob GetJob(string jobId)
491501
if (job.Status == TestJobStatus.Running && job.TotalTests == null)
492502
{
493503
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
494-
if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > InitializationTimeoutMs)
504+
long initTimeout = job.InitTimeoutMs > 0 ? job.InitTimeoutMs : DefaultInitializationTimeoutMs;
505+
if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout)
495506
{
496-
McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {InitializationTimeoutMs}ms, auto-failing");
507+
McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {initTimeout}ms, auto-failing");
497508
job.Status = TestJobStatus.Failed;
498509
job.Error = "Test job failed to initialize (tests did not start within timeout)";
499510
job.FinishedUnixMs = now;

MCPForUnity/Editor/Tools/Animation/ClipCreate.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using Newtonsoft.Json.Linq;
66
using MCPForUnity.Editor.Helpers;
7+
using MCPForUnity.Runtime.Helpers;
78
using UnityEditor;
89
using UnityEngine;
910

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

493494
// Fallback: search all loaded assemblies
494-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
495+
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
495496
{
496497
type = assembly.GetType(typeName);
497498
if (type != null) return type;

MCPForUnity/Editor/Tools/CommandRegistry.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using MCPForUnity.Editor.Helpers;
77
using MCPForUnity.Editor.Resources;
8+
using MCPForUnity.Runtime.Helpers;
89
using Newtonsoft.Json;
910
using Newtonsoft.Json.Linq;
1011

@@ -59,7 +60,7 @@ private static void AutoDiscoverCommands()
5960
{
6061
try
6162
{
62-
var allTypes = AppDomain.CurrentDomain.GetAssemblies()
63+
var allTypes = UnityAssembliesCompat.GetLoadedAssemblies()
6364
.Where(a => !a.IsDynamic)
6465
.SelectMany(a =>
6566
{

MCPForUnity/Editor/Tools/ExecuteCode.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Reflection;
77
using System.Text;
88
using MCPForUnity.Editor.Helpers;
9+
using MCPForUnity.Runtime.Helpers;
910
using Microsoft.CSharp;
1011
using Newtonsoft.Json.Linq;
1112
using UnityEngine;
@@ -360,7 +361,7 @@ private static string[] ResolveAssemblyPaths()
360361
{
361362
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
362363

363-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
364+
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
364365
{
365366
try
366367
{

0 commit comments

Comments
 (0)