diff --git a/MCPForUnity/Editor/Helpers/ComponentOps.cs b/MCPForUnity/Editor/Helpers/ComponentOps.cs index b1ccdc855..9a481856b 100644 --- a/MCPForUnity/Editor/Helpers/ComponentOps.cs +++ b/MCPForUnity/Editor/Helpers/ComponentOps.cs @@ -748,6 +748,18 @@ internal static bool SetObjectReference(SerializedProperty prop, JToken value, o return AssignObjectReference(prop, resolved, null, out error); } + // Try as asset GUID (32-char hex string) + if (strVal.Length == 32 && IsHexString(strVal)) + { + string assetPath = AssetDatabase.GUIDToAssetPath(strVal); + if (!string.IsNullOrEmpty(assetPath)) + { + var resolved = AssetDatabase.LoadAssetAtPath(assetPath); + if (resolved != null) + return AssignObjectReference(prop, resolved, null, out error); + } + } + // Fall back to scene hierarchy lookup by name. return ResolveSceneObjectByName(prop, strVal, null, out error); } @@ -969,6 +981,16 @@ private static long GetSpriteFileId(Sprite sprite) return 0; } } + + private static bool IsHexString(string str) + { + foreach (char c in str) + { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) + return false; + } + return true; + } } } diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs index 160d668ff..c14482ebe 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs @@ -111,7 +111,11 @@ void Tick() private static readonly string[] ValidCategories = new[] { - "Render", "Scripts", "Memory", "Physics", "Physics2D", "Animation", + "Render", "Scripts", "Memory", "Physics", +#if UNITY_2022_2_OR_NEWER + "Physics2D", +#endif + "Animation", "Audio", "Lighting", "Network", "Gui", "UI", "Ai", "Video", "Loading", "Input", "Vr", "Internal", "Particles", "FileIO", "VirtualTexturing" }; @@ -125,7 +129,9 @@ void Tick() case "scripts": return ProfilerCategory.Scripts; case "memory": return ProfilerCategory.Memory; case "physics": return ProfilerCategory.Physics; +#if UNITY_2022_2_OR_NEWER case "physics2d": return ProfilerCategory.Physics2D; +#endif case "animation": return ProfilerCategory.Animation; case "audio": return ProfilerCategory.Audio; case "lighting": return ProfilerCategory.Lighting; diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs index a11ded2e8..23a3b9614 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/FrameTimingOps.cs @@ -8,6 +8,7 @@ internal static class FrameTimingOps { internal static object GetFrameTiming(JObject @params) { +#if UNITY_2022_2_OR_NEWER if (!FrameTimingManager.IsFeatureEnabled()) { return new ErrorResponse( @@ -15,6 +16,7 @@ internal static object GetFrameTiming(JObject @params) + "Enable it in Project Settings > Player > Other Settings > 'Frame Timing Stats', " + "or use a Development Build (always enabled)."); } +#endif FrameTimingManager.CaptureFrameTimings(); var timings = new FrameTiming[1]; @@ -33,12 +35,16 @@ internal static object GetFrameTiming(JObject @params) { available = true, cpu_frame_time_ms = t.cpuFrameTime, +#if UNITY_2022_2_OR_NEWER cpu_main_thread_frame_time_ms = t.cpuMainThreadFrameTime, cpu_main_thread_present_wait_time_ms = t.cpuMainThreadPresentWaitTime, cpu_render_thread_frame_time_ms = t.cpuRenderThreadFrameTime, +#endif gpu_frame_time_ms = t.gpuFrameTime, +#if UNITY_2022_2_OR_NEWER frame_start_timestamp = t.frameStartTimestamp, first_submit_timestamp = t.firstSubmitTimestamp, +#endif cpu_time_present_called = t.cpuTimePresentCalled, cpu_time_frame_complete = t.cpuTimeFrameComplete, height_scale = t.heightScale, diff --git a/MCPForUnity/Editor/Tools/UnityReflect.cs b/MCPForUnity/Editor/Tools/UnityReflect.cs index 576026d84..0af4f6187 100644 --- a/MCPForUnity/Editor/Tools/UnityReflect.cs +++ b/MCPForUnity/Editor/Tools/UnityReflect.cs @@ -170,6 +170,23 @@ private static object GetTypeInfo(ToolParams p) }); } + // Open generic type definitions (e.g. List) segfault Mono on Unity 2021.3 + // in mono_metadata_generic_param_equal_internal when any member reflection is + // performed. Return minimal info using only safe property accesses. + if (type.IsGenericTypeDefinition) + { + return new SuccessResponse($"Type info for '{type.Name}'.", new + { + found = true, + name = type.Name, + full_name = type.FullName, + @namespace = type.Namespace, + assembly = type.Assembly.GetName().Name, + is_generic_type_definition = true, + hint = "Open generic type — consult docs for member details." + }); + } + var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; var methods = type.GetMethods(flags) @@ -272,6 +289,19 @@ private static object GetMemberInfo(ToolParams p) }); } + if (type.IsGenericTypeDefinition) + { + return new SuccessResponse( + $"Open generic type '{type.Name}' — consult docs for member details.", new + { + found = false, + type_name = type.FullName, + member_name = memberName, + is_generic_type_definition = true, + hint = "Open generic type — consult docs for member details." + }); + } + // Use flags without DeclaredOnly to find inherited members var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; diff --git a/Server/tests/test_cli_commands_characterization.py b/Server/tests/test_cli_commands_characterization.py index 21cff3d74..b5e8ad3fa 100644 --- a/Server/tests/test_cli_commands_characterization.py +++ b/Server/tests/test_cli_commands_characterization.py @@ -807,7 +807,7 @@ def test_prefab_workflow_open_modify_save(self, runner, mock_config): assert result.exit_code == 0 # Save - result = runner.invoke(prefab, ["save", "--force"]) + result = runner.invoke(prefab, ["save"]) assert result.exit_code == 0 # Close diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGraphicsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGraphicsTests.cs index c70e8be6c..6b897a220 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGraphicsTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGraphicsTests.cs @@ -15,6 +15,7 @@ public class ManageGraphicsTests private const string TempRoot = "Assets/Temp/ManageGraphicsTests"; private bool _hasVolumeSystem; private bool _hasURP; + private bool _hasHDRP; [SetUp] public void SetUp() @@ -28,6 +29,7 @@ public void SetUp() var data = pingResult["data"]; _hasVolumeSystem = data?.Value("hasVolumeSystem") ?? false; _hasURP = data?.Value("hasURP") ?? false; + _hasHDRP = data?.Value("hasHDRP") ?? false; } } @@ -72,7 +74,7 @@ public void HandleCommand_MissingAction_ReturnsError() { var result = ToJObject(ManageGraphics.HandleCommand(new JObject())); Assert.IsFalse(result.Value("success")); - Assert.That(result["message"].ToString(), Does.Contain("action")); + Assert.That(result["error"].ToString(), Does.Contain("action")); } [Test] @@ -81,7 +83,7 @@ public void HandleCommand_UnknownAction_ReturnsError() var result = ToJObject(ManageGraphics.HandleCommand( new JObject { ["action"] = "bogus_action" })); Assert.IsFalse(result.Value("success")); - Assert.That(result["message"].ToString(), Does.Contain("Unknown action")); + Assert.That(result["error"].ToString(), Does.Contain("Unknown action")); } [Test] @@ -539,7 +541,7 @@ public void BakeSetProbePositions_WrongComponent_ReturnsError() ["positions"] = new JArray { new JArray(0, 0, 0) } })); Assert.IsFalse(result.Value("success")); - Assert.That(result["message"].ToString(), Does.Contain("LightProbeGroup")); + Assert.That(result["error"].ToString(), Does.Contain("LightProbeGroup")); } // ===================================================================== @@ -598,7 +600,7 @@ public void StatsSetSceneDebug_InvalidMode_ReturnsError() ["mode"] = "InvalidMode" })); Assert.IsFalse(result.Value("success")); - Assert.That(result["message"].ToString(), Does.Contain("Valid:")); + Assert.That(result["error"].ToString(), Does.Contain("Valid:")); } // ===================================================================== @@ -618,6 +620,7 @@ public void PipelineGetInfo_ReturnsPipelineName() [Test] public void PipelineGetSettings_ReturnsSettings() { + Assume.That(_hasURP || _hasHDRP, "Built-in pipeline has no settings asset — skipping."); var result = ToJObject(ManageGraphics.HandleCommand( new JObject { ["action"] = "pipeline_get_settings" })); Assert.IsTrue(result.Value("success"), result.ToString()); @@ -635,7 +638,7 @@ public void PipelineSetQuality_InvalidLevel_ReturnsError() ["level"] = "NonExistentLevel" })); Assert.IsFalse(result.Value("success")); - Assert.That(result["message"].ToString(), Does.Contain("Available:")); + Assert.That(result["error"].ToString(), Does.Contain("Available:")); } // ===================================================================== diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePhysicsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePhysicsTests.cs index 000914d1f..ea6490b07 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePhysicsTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePhysicsTests.cs @@ -768,8 +768,9 @@ public void SetCollisionMatrix_MissingParams_ReturnsError() [Test] public void Raycast_HitsCollider() { + // Use an offset origin/direction to avoid hitting unrelated scene objects var go = new GameObject("PhysTest_RayTarget"); - go.transform.position = new Vector3(0, 0, 5); + go.transform.position = new Vector3(500, 500, 5); var col = go.AddComponent(); col.size = new Vector3(10, 10, 1); UnityEngine.Physics.SyncTransforms(); @@ -777,7 +778,7 @@ public void Raycast_HitsCollider() var result = ToJObject(ManagePhysics.HandleCommand(new JObject { ["action"] = "raycast", - ["origin"] = new JArray(0, 0, 0), + ["origin"] = new JArray(500, 500, 0), ["direction"] = new JArray(0, 0, 1), ["max_distance"] = 100 })); diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs index f8e140e06..cbc28f9d1 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageProBuilderTests.cs @@ -53,8 +53,8 @@ public void HandleCommand_UnknownAction_ReturnsError() var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success"), result.ToString()); - Assert.That(result["error"]?.ToString() ?? result["message"]?.ToString(), - Does.Contain("Unknown action")); + var errorMsg = result["error"]?.ToString() ?? result["message"]?.ToString(); + Assert.That(errorMsg, Does.Contain("Unknown action").Or.Contain("not installed")); } [Test] diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs index 412c52a7c..c6bd43f7d 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneHierarchyPagingTests.cs @@ -115,7 +115,7 @@ public void Screenshot_SceneViewRejectsSupersizeAboveOne() var response = raw as JObject ?? JObject.FromObject(raw); Assert.IsFalse(response.Value("success"), response.ToString()); - StringAssert.Contains("does not support super_size above 1", response.Value("message")); + StringAssert.Contains("does not support super_size above 1", response.Value("error")); } [Test] @@ -182,7 +182,7 @@ public void Screenshot_ViewTargetAcceptedForGameView() // Should attempt positioned capture and fail to resolve the GO — not reject the param Assert.IsFalse(response.Value("success"), response.ToString()); - StringAssert.Contains("not found", response.Value("message")); + StringAssert.Contains("not found", response.Value("error")); } [Test] diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneMultiSceneTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneMultiSceneTests.cs index df1bc9af0..185449c94 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneMultiSceneTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageSceneMultiSceneTests.cs @@ -78,7 +78,7 @@ public void ModifyBuildSettings_RedirectsToManageBuild() var result = ManageScene.HandleCommand(p); var r = result as JObject ?? JObject.FromObject(result); Assert.IsFalse(r.Value("success")); - Assert.IsTrue(r.Value("message").Contains("manage_build")); + Assert.IsTrue(r.Value("error").Contains("manage_build")); } [Test] diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs index 4518c805d..14166c7af 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageUITests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using UnityEditor; using UnityEngine; +using UnityEngine.TestTools; using UnityEngine.UIElements; using MCPForUnity.Editor.Tools; using static MCPForUnityTests.Editor.TestUtilities; @@ -83,8 +84,10 @@ public void Create_Uxml_CreatesFile() path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); Assert.IsTrue(File.Exists(fullPath), $"File should exist at {fullPath}"); + // EnsureEditorExtensionMode may inject editor-extension-mode attribute string actual = File.ReadAllText(fullPath); - Assert.AreEqual(content, actual); + Assert.That(actual, Does.Contain("ui:UXML")); + Assert.That(actual, Does.Contain("ui:Label")); } [Test] @@ -134,13 +137,14 @@ public void Create_MissingContents_ReturnsError() public void Create_AlreadyExists_ReturnsError() { string path = $"{TempRoot}/Exists_{Guid.NewGuid():N}.uxml"; + string content = ""; // Create first time ManageUI.HandleCommand(new JObject { ["action"] = "create", ["path"] = path, - ["contents"] = "", + ["contents"] = content, }); // Try to create again @@ -148,7 +152,7 @@ public void Create_AlreadyExists_ReturnsError() { ["action"] = "create", ["path"] = path, - ["contents"] = "", + ["contents"] = content, })); Assert.IsFalse(result.Value("success")); @@ -175,7 +179,9 @@ public void Create_WithBase64EncodedContents_Decodes() string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); string actual = File.ReadAllText(fullPath); - Assert.AreEqual(content, actual); + // EnsureEditorExtensionMode may inject editor-extension-mode attribute + Assert.That(actual, Does.Contain("ui:UXML")); + Assert.That(actual, Does.Contain("UnityEngine.UIElements")); } // ---- Read file ---- @@ -184,7 +190,7 @@ public void Create_WithBase64EncodedContents_Decodes() public void Read_ExistingFile_ReturnsContents() { string path = $"{TempRoot}/ReadTest_{Guid.NewGuid():N}.uxml"; - string content = ""; + string content = ""; ManageUI.HandleCommand(new JObject { @@ -202,7 +208,8 @@ public void Read_ExistingFile_ReturnsContents() Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); - Assert.AreEqual(content, data.Value("contents")); + // EnsureEditorExtensionMode may inject editor-extension-mode attribute + Assert.That(data.Value("contents"), Does.Contain("ui:UXML")); } [Test] @@ -675,19 +682,28 @@ public void Create_MissingNamespace_WritesWithWarning() string path = $"{TempRoot}/NoNs_{Guid.NewGuid():N}.uxml"; string content = ""; - var result = ToJObject(ManageUI.HandleCommand(new JObject + // Unity's UXML importer logs an error for undeclared 'ui' prefix + LogAssert.ignoreFailingMessages = true; + try { - ["action"] = "create", - ["path"] = path, - ["contents"] = content, - })); + var result = ToJObject(ManageUI.HandleCommand(new JObject + { + ["action"] = "create", + ["path"] = path, + ["contents"] = content, + })); - Assert.IsTrue(result.Value("success"), result.ToString()); - var data = result["data"] as JObject; - Assert.IsNotNull(data); - var warnings = data["validationWarnings"] as JArray; - Assert.IsNotNull(warnings, "Should have validationWarnings"); - Assert.That(warnings.ToString(), Does.Contain("Missing namespace")); + Assert.IsTrue(result.Value("success"), result.ToString()); + var data = result["data"] as JObject; + Assert.IsNotNull(data); + var warnings = data["validationWarnings"] as JArray; + Assert.IsNotNull(warnings, "Should have validationWarnings"); + Assert.That(warnings.ToString(), Does.Contain("Missing namespace")); + } + finally + { + LogAssert.ignoreFailingMessages = false; + } } [Test] @@ -715,18 +731,27 @@ public void Create_WrongRootElement_WritesWithWarning() string path = $"{TempRoot}/WrongRoot_{Guid.NewGuid():N}.uxml"; string content = "
"; - var result = ToJObject(ManageUI.HandleCommand(new JObject + // Unity's UXML importer logs an error about expected root element + LogAssert.ignoreFailingMessages = true; + try { - ["action"] = "create", - ["path"] = path, - ["contents"] = content, - })); + var result = ToJObject(ManageUI.HandleCommand(new JObject + { + ["action"] = "create", + ["path"] = path, + ["contents"] = content, + })); - Assert.IsTrue(result.Value("success"), result.ToString()); - var data = result["data"] as JObject; - var warnings = data["validationWarnings"] as JArray; - Assert.IsNotNull(warnings, "Should have validationWarnings"); - Assert.That(warnings.ToString(), Does.Contain("Root element")); + Assert.IsTrue(result.Value("success"), result.ToString()); + var data = result["data"] as JObject; + var warnings = data["validationWarnings"] as JArray; + Assert.IsNotNull(warnings, "Should have validationWarnings"); + Assert.That(warnings.ToString(), Does.Contain("Root element")); + } + finally + { + LogAssert.ignoreFailingMessages = false; + } } [Test] @@ -769,11 +794,12 @@ public void Update_MalformedXml_ReturnsError_FileNotChanged() Assert.IsFalse(result.Value("success")); Assert.That(result["error"].ToString(), Does.Contain("Malformed XML")); - // Verify original content was preserved + // Verify original content was preserved (EnsureEditorExtensionMode may have injected attribute) string fullPath = Path.Combine(Application.dataPath, path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar); string actual = File.ReadAllText(fullPath); - Assert.AreEqual(original, actual, "Original file content should be preserved"); + Assert.That(actual, Does.Contain("ui:UXML"), "Original file content should be preserved"); + Assert.That(actual, Does.Not.Contain(""), "Malformed content should not be written"); } [Test] diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/UnityReflectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/UnityReflectTests.cs index 6e13ba9f0..c5263f162 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/UnityReflectTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/UnityReflectTests.cs @@ -20,7 +20,7 @@ private static JObject Invoke(string action, JObject extraParams = null) [Test] public void GetType_Transform_ReturnsFound() { - var jo = Invoke("get_type", new JObject { ["class_name"] = "Transform" }); + var jo = Invoke("get_type", new JObject { ["class_name"] = "UnityEngine.Transform" }); Assert.IsTrue((bool)jo["success"]); var data = jo["data"]; @@ -120,7 +120,7 @@ public void GetMember_Property_ReturnsPropertyInfo() { var jo = Invoke("get_member", new JObject { - ["class_name"] = "Transform", + ["class_name"] = "UnityEngine.Transform", ["member_name"] = "position" });