Skip to content

Commit 6e4302d

Browse files
committed
Merge remote-tracking branch 'upstream/beta' into fix/composited-screenshot-wait-for-end-of-frame
# Conflicts: # MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
2 parents 2a7ec42 + 0e02e5d commit 6e4302d

7 files changed

Lines changed: 180 additions & 23 deletions

File tree

MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
using System;
12
using UnityEditor;
23
using UnityEditor.Build;
34

45
namespace MCPForUnity.Editor.Tools.Build
56
{
67
public static class BuildTargetMapping
78
{
9+
private const string VisionOSName = "VisionOS";
10+
811
public static bool TryResolveBuildTarget(string name, out BuildTarget target)
912
{
1013
if (string.IsNullOrEmpty(name))
@@ -24,13 +27,12 @@ public static bool TryResolveBuildTarget(string name, out BuildTarget target)
2427
case "webgl": target = BuildTarget.WebGL; return true;
2528
case "uwp": target = BuildTarget.WSAPlayer; return true;
2629
case "tvos": target = BuildTarget.tvOS; return true;
27-
// BuildTarget.VisionOS exists only in Unity 2023.2+ and late 2022.3 patches
28-
#if UNITY_2023_2_OR_NEWER
29-
case "visionos": target = BuildTarget.VisionOS; return true;
30-
#endif
3130
default:
32-
if (System.Enum.TryParse(name, true, out target))
31+
if (TryParseDefinedBuildTarget(name, out target))
32+
{
3333
return true;
34+
}
35+
3436
target = default;
3537
return false;
3638
}
@@ -50,10 +52,14 @@ public static BuildTargetGroup GetTargetGroup(BuildTarget target)
5052
case BuildTarget.WebGL: return BuildTargetGroup.WebGL;
5153
case BuildTarget.WSAPlayer: return BuildTargetGroup.WSA;
5254
case BuildTarget.tvOS: return BuildTargetGroup.tvOS;
53-
#if UNITY_2023_2_OR_NEWER
54-
case BuildTarget.VisionOS: return BuildTargetGroup.VisionOS;
55-
#endif
56-
default: return BuildTargetGroup.Unknown;
55+
default:
56+
if (IsVisionOSTarget(target)
57+
&& Enum.TryParse(VisionOSName, true, out BuildTargetGroup visionOSGroup))
58+
{
59+
return visionOSGroup;
60+
}
61+
62+
return BuildTargetGroup.Unknown;
5763
}
5864
}
5965

@@ -67,12 +73,61 @@ public static string TryResolveNamedBuildTarget(string name, out NamedBuildTarge
6773
if (!TryResolveBuildTarget(name, out var buildTarget))
6874
{
6975
namedTarget = default;
70-
return $"Unknown build target: '{name}'. Valid targets: windows64, osx, linux64, android, ios, webgl, uwp, tvos, visionos";
76+
return GetUnknownBuildTargetMessage(name);
77+
}
78+
79+
var targetGroup = GetTargetGroup(buildTarget);
80+
if (targetGroup == BuildTargetGroup.Unknown)
81+
{
82+
namedTarget = default;
83+
return IsVisionOSTarget(buildTarget)
84+
? "VisionOS build target is available, but its BuildTargetGroup is not exposed by this Unity editor installation."
85+
: $"Build target group could not be resolved for target '{buildTarget}'.";
7186
}
72-
namedTarget = GetNamedBuildTarget(buildTarget);
87+
88+
namedTarget = NamedBuildTarget.FromBuildTargetGroup(targetGroup);
7389
return null;
7490
}
7591

92+
public static string GetUnknownBuildTargetMessage(string name)
93+
{
94+
if (string.Equals(name, "visionos", StringComparison.OrdinalIgnoreCase))
95+
{
96+
return "VisionOS build target is not available in this Unity editor installation. "
97+
+ "Install the visionOS build support module or use a Unity version/configuration that exposes BuildTarget.VisionOS.";
98+
}
99+
100+
return $"Unknown build target: '{name}'. Valid targets: {GetValidTargetsList()}.";
101+
}
102+
103+
private static string GetValidTargetsList()
104+
{
105+
string validTargets = "windows64, osx, linux64, android, ios, webgl, uwp, tvos";
106+
if (TryParseDefinedBuildTarget(VisionOSName, out _))
107+
{
108+
validTargets += ", visionos";
109+
}
110+
111+
return validTargets;
112+
}
113+
114+
private static bool IsVisionOSTarget(BuildTarget target)
115+
{
116+
return string.Equals(target.ToString(), VisionOSName, StringComparison.OrdinalIgnoreCase);
117+
}
118+
119+
private static bool TryParseDefinedBuildTarget(string name, out BuildTarget target)
120+
{
121+
target = default;
122+
if (int.TryParse(name, out _))
123+
{
124+
return false;
125+
}
126+
127+
return Enum.TryParse(name, true, out target)
128+
&& Enum.IsDefined(typeof(BuildTarget), target);
129+
}
130+
76131
public static string GetDefaultOutputPath(BuildTarget target, string productName)
77132
{
78133
string basePath = $"Builds/{target}";

MCPForUnity/Editor/Tools/ManageBuild.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static object HandleBuild(ToolParams p)
6464

6565
string targetName = p.Get("target");
6666
if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target))
67-
return new ErrorResponse($"Unknown target '{targetName}'.");
67+
return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName));
6868

6969
var group = BuildTargetMapping.GetTargetGroup(target);
7070
if (!BuildPipeline.IsBuildTargetSupported(group, target))
@@ -221,7 +221,7 @@ private static object HandlePlatform(ToolParams p)
221221

222222
// Switch platform
223223
if (!BuildTargetMapping.TryResolveBuildTarget(targetName, out var target))
224-
return new ErrorResponse($"Unknown target '{targetName}'.");
224+
return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(targetName));
225225

226226
var group = BuildTargetMapping.GetTargetGroup(target);
227227
if (!BuildPipeline.IsBuildTargetSupported(group, target))
@@ -442,7 +442,7 @@ private static object HandleBatch(ToolParams p)
442442
foreach (var t in targets)
443443
{
444444
if (!BuildTargetMapping.TryResolveBuildTarget(t, out var bt))
445-
return new ErrorResponse($"Unknown target '{t}' in batch.");
445+
return new ErrorResponse(BuildTargetMapping.GetUnknownBuildTargetMessage(t));
446446
var btGroup = BuildTargetMapping.GetTargetGroup(bt);
447447
if (!BuildPipeline.IsBuildTargetSupported(btGroup, bt))
448448
return new ErrorResponse(

MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static class ScreenshotUtility
5353
private static bool s_loggedLegacyScreenCaptureFallback;
5454
private static bool? s_screenCaptureModuleAvailable;
5555
private static System.Reflection.MethodInfo s_captureScreenshotMethod;
56+
private static System.Reflection.MethodInfo s_captureScreenshotAsTextureMethod;
5657

5758
/// <summary>
5859
/// Checks if the Screen Capture module (com.unity.modules.screencapture) is enabled.
@@ -72,12 +73,26 @@ public static bool IsScreenCaptureModuleAvailable
7273
{
7374
s_captureScreenshotMethod = screenCaptureType.GetMethod("CaptureScreenshot",
7475
new Type[] { typeof(string), typeof(int) });
76+
s_captureScreenshotAsTextureMethod = screenCaptureType.GetMethod("CaptureScreenshotAsTexture",
77+
new Type[] { typeof(int) });
7578
}
7679
}
7780
return s_screenCaptureModuleAvailable.Value;
7881
}
7982
}
8083

84+
/// <summary>
85+
/// Reflective invocation of ScreenCapture.CaptureScreenshotAsTexture(int). Returns
86+
/// null when the Screen Capture module is disabled. Centralised so the only direct
87+
/// reference to ScreenCapture lives here.
88+
/// </summary>
89+
internal static Texture2D InvokeCaptureScreenshotAsTexture(int superSize)
90+
{
91+
if (!IsScreenCaptureModuleAvailable || s_captureScreenshotAsTextureMethod == null)
92+
return null;
93+
return s_captureScreenshotAsTextureMethod.Invoke(null, new object[] { superSize }) as Texture2D;
94+
}
95+
8196
/// <summary>
8297
/// Error message to display when Screen Capture module is not available.
8398
/// </summary>
@@ -278,7 +293,7 @@ public static ScreenshotCaptureResult CaptureComposited(
278293
int maxResolution = 0,
279294
string folderOverride = null)
280295
{
281-
if (!IsScreenCaptureModuleAvailable)
296+
if (!IsScreenCaptureModuleAvailable || s_captureScreenshotAsTextureMethod == null)
282297
{
283298
var fallbackCamera = FindAvailableCamera();
284299
if (fallbackCamera != null)
@@ -300,9 +315,9 @@ public static ScreenshotCaptureResult CaptureComposited(
300315
// composited; route through WaitForEndOfFrame instead.
301316
tex = Application.isPlaying
302317
? CaptureCompositedAfterFrame(result.SuperSize)
303-
: ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
318+
: InvokeCaptureScreenshotAsTexture(result.SuperSize);
304319
#else
305-
tex = ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
320+
tex = InvokeCaptureScreenshotAsTexture(result.SuperSize);
306321
#endif
307322
if (tex == null)
308323
{
@@ -842,7 +857,7 @@ private System.Collections.IEnumerator Start()
842857
{
843858
yield return new WaitForEndOfFrame();
844859
Texture2D tex = null;
845-
try { tex = ScreenCapture.CaptureScreenshotAsTexture(_superSize); }
860+
try { tex = ScreenshotUtility.InvokeCaptureScreenshotAsTexture(_superSize); }
846861
catch (Exception ex) { Debug.LogError($"[MCP for Unity] CaptureScreenshotAsTexture failed: {ex.Message}"); }
847862
_onComplete?.Invoke(tex);
848863
Destroy(gameObject);

MCPForUnity/Runtime/Helpers/UnityPhysicsCompat.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ namespace MCPForUnity.Runtime.Helpers
1717
///
1818
/// We use reflection rather than direct property access so calls stay clean of
1919
/// CS0618 warnings AND survive eventual removal of the obsolete property without
20-
/// a recompile of this package.
20+
/// a recompile of this package. Type lookups go through <see cref="Type.GetType(string)"/>
21+
/// so this file compiles even when the Physics 2D built-in module is disabled in
22+
/// the Package Manager.
2123
/// </summary>
2224
public static class UnityPhysicsCompat
2325
{
26+
// Assembly-qualified name — resolved at runtime so the file compiles when the
27+
// Physics 2D built-in module is disabled in the Package Manager.
28+
private const string Physics2DTypeName = "UnityEngine.Physics2D, UnityEngine.Physics2DModule";
2429
/// <summary>
2530
/// Cross-version description of the 3D physics simulation mode.
2631
/// On 2022.2+ this maps onto <c>UnityEngine.SimulationMode</c>; on older
@@ -46,9 +51,13 @@ private static PropertyInfo Physics2DAutoSyncProp
4651
if (!_physics2DProbed)
4752
{
4853
_physics2DProbed = true;
49-
_physics2DAutoSync = typeof(Physics2D).GetProperty(
50-
"autoSyncTransforms",
51-
BindingFlags.Public | BindingFlags.Static);
54+
var physics2DType = Type.GetType(Physics2DTypeName);
55+
if (physics2DType != null)
56+
{
57+
_physics2DAutoSync = physics2DType.GetProperty(
58+
"autoSyncTransforms",
59+
BindingFlags.Public | BindingFlags.Static);
60+
}
5261
}
5362
return _physics2DAutoSync;
5463
}

MCPForUnity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.coplaydev.unity-mcp",
3-
"version": "9.7.2-beta.1",
3+
"version": "9.7.2-beta.3",
44
"displayName": "MCP for Unity",
55
"description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
66
"unity": "2021.3",
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using MCPForUnity.Editor.Tools.Build;
3+
using NUnit.Framework;
4+
using UnityEditor;
5+
6+
namespace MCPForUnity.Tests.EditMode.Tools
7+
{
8+
[TestFixture]
9+
public class BuildTargetMappingTests
10+
{
11+
[TestCase("windows64", BuildTarget.StandaloneWindows64)]
12+
[TestCase("macos", BuildTarget.StandaloneOSX)]
13+
[TestCase("linux", BuildTarget.StandaloneLinux64)]
14+
[TestCase("tvos", BuildTarget.tvOS)]
15+
public void TryResolveBuildTarget_KnownAliasesResolve(string name, BuildTarget expected)
16+
{
17+
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget(name, out var target));
18+
Assert.AreEqual(expected, target);
19+
}
20+
21+
[Test]
22+
public void TryResolveBuildTarget_NumericInputDoesNotResolve()
23+
{
24+
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("5", out _));
25+
}
26+
27+
[Test]
28+
public void TryResolveNamedBuildTarget_UnknownTargetListsOnlyAvailableTargets()
29+
{
30+
string error = BuildTargetMapping.TryResolveNamedBuildTarget("not-a-target", out _);
31+
32+
Assert.IsNotNull(error);
33+
StringAssert.Contains("windows64", error);
34+
35+
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
36+
if (visionOSAvailable)
37+
{
38+
StringAssert.Contains("visionos", error);
39+
}
40+
else
41+
{
42+
Assert.IsFalse(error.Contains("visionos"));
43+
}
44+
}
45+
46+
[Test]
47+
public void TryResolveNamedBuildTarget_VisionOSUnavailableReturnsHelpfulError()
48+
{
49+
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
50+
string error = BuildTargetMapping.TryResolveNamedBuildTarget("visionos", out _);
51+
52+
if (visionOSAvailable)
53+
{
54+
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
55+
Assert.IsTrue(
56+
error == null || error.Contains("VisionOS"),
57+
$"Expected no error or a VisionOS-specific error, got: {error}");
58+
}
59+
else
60+
{
61+
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
62+
Assert.IsNotNull(error);
63+
StringAssert.Contains("VisionOS build target is not available", error);
64+
}
65+
}
66+
}
67+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/BuildTargetMappingTests.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.

0 commit comments

Comments
 (0)