From 3848e79e7fa173a17fc48b3b55406b79217fb650 Mon Sep 17 00:00:00 2001 From: RoyougiShiki Date: Mon, 10 Aug 2026 18:01:26 +0800 Subject: [PATCH 1/9] compat: Unity 2020.3 LTS support (v10.1.2 baseline) - C#9 -> C#8: target-typed new(), is not, or-patterns, switch-arms (95+ sites) - .NET 2.0 API shims: Contains/Join/Range/Math.Clamp/ArgumentList etc (37 sites) - 2021.2+ Unity APIs behind #if UNITY_2021_2_OR_NEWER with equivalent 2020.3 impls: NamedBuildTarget, PrefabStageUtility (Experimental ns), subtarget, GetAllRegisteredPackages, AddAndRemoveRequest, ShaderPropertyType.Int, ProfilerCategory - New CompatDropdownField (DropdownField not in 2020.3): 2021.2+ native, 2020.3 self-drawn equivalent with UxmlFactory; 4 UXMLs migrated - Verified: Unity 2020.3.24f1 batchmode compile 0 error/0 warning, all 11 UXMLs load, window/dropdown/tool-routing runtime checks pass - docs/UNITY_2020_3_COMPAT.md: full patch inventory for sync strategy --- .../Clients/McpClientConfiguratorBase.cs | 6 +- .../LinuxPlatformDetector.cs | 2 +- .../MacOSPlatformDetector.cs | 2 +- .../WindowsPlatformDetector.cs | 2 +- MCPForUnity/Editor/External/Tommy.cs | 36 +- .../Editor/Helpers/AssetPathUtility.cs | 12 +- .../Editor/Helpers/CodexConfigHelper.cs | 2 +- .../Editor/Helpers/GameObjectLookup.cs | 4 + .../Editor/Helpers/HttpEndpointUtility.cs | 2 +- .../Editor/Helpers/McpConfigurationHelper.cs | 2 +- MCPForUnity/Editor/Helpers/McpLogRecord.cs | 5 +- MCPForUnity/Editor/Helpers/PortManager.cs | 2 +- .../Helpers/ProcessArgumentListCompat.cs | 43 + .../Helpers/ProcessArgumentListCompat.cs.meta | 11 + .../Editor/Helpers/ProjectIdentityUtility.cs | 2 +- .../Editor/Helpers/UnityTypeResolver.cs | 14 +- MCPForUnity/Editor/Helpers/VectorParsing.cs | 2 +- MCPForUnity/Editor/Models/McpClient.cs | 2 +- .../Editor/Resources/Editor/GetPrefabStage.cs | 4 + .../SecureKeyStore/EncryptedFileKeyStore.cs | 7 +- .../SecureKeyStore/LinuxSecretToolKeyStore.cs | 25 +- .../SecureKeyStore/MacKeychainKeyStore.cs | 3 +- .../Services/AssetGen/AssetGenJobManager.cs | 16 +- .../AssetGen/Import/ModelImportPipeline.cs | 2 +- .../Services/AssetGen/Providers/LocalImage.cs | 2 +- .../Editor/Services/EditorStateCache.cs | 2 +- .../Services/IClientConfigurationService.cs | 2 +- .../Editor/Services/PackageJobManager.cs | 28 +- .../Editor/Services/PackageUpdateService.cs | 2 +- .../Editor/Services/PathResolverService.cs | 2 +- MCPForUnity/Editor/Services/TestJobManager.cs | 4 +- MCPForUnity/Editor/Services/TestRunStatus.cs | 2 +- .../Transport/TransportCommandDispatcher.cs | 11 +- .../Transport/Transports/StdioBridgeHost.cs | 18 +- .../Transports/WebSocketTransportClient.cs | 6 +- .../Editor/Setup/McpForUnitySkillInstaller.cs | 4 +- MCPForUnity/Editor/Setup/SkillSyncService.cs | 18 +- .../Editor/Tools/Animation/ClipCreate.cs | 6 +- .../Tools/Animation/ControllerCreate.cs | 2 +- .../Tools/AssetGen/AssetGenToolHelpers.cs | 2 +- .../Editor/Tools/AssetGen/ImportModel.cs | 2 +- MCPForUnity/Editor/Tools/BatchExecute.cs | 6 +- MCPForUnity/Editor/Tools/Build/BuildJob.cs | 6 +- MCPForUnity/Editor/Tools/Build/BuildRunner.cs | 8 +- .../Editor/Tools/Build/BuildSettingsHelper.cs | 12 +- .../Editor/Tools/Build/BuildTargetMapping.cs | 13 +- .../Editor/Tools/Cameras/CameraCreate.cs | 2 +- .../Editor/Tools/Cameras/CameraHelpers.cs | 7 +- MCPForUnity/Editor/Tools/CommandRegistry.cs | 4 +- MCPForUnity/Editor/Tools/ExecuteCode.cs | 2 +- .../Tools/GameObjects/ComponentResolver.cs | 2 +- .../GameObjects/GameObjectComponentHelpers.cs | 2 +- .../Tools/GameObjects/GameObjectModify.cs | 3 + .../GameObjects/ManageGameObjectCommon.cs | 6 +- .../Editor/Tools/Graphics/GraphicsHelpers.cs | 6 +- .../Editor/Tools/Graphics/LightBakingOps.cs | 6 + .../Tools/Graphics/RenderPipelineOps.cs | 9 +- .../Editor/Tools/Graphics/SkyboxOps.cs | 8 +- .../Editor/Tools/Graphics/VolumeOps.cs | 9 +- MCPForUnity/Editor/Tools/ManageBuild.cs | 10 + MCPForUnity/Editor/Tools/ManageComponents.cs | 3 + MCPForUnity/Editor/Tools/ManagePackages.cs | 33 +- MCPForUnity/Editor/Tools/ManageScene.cs | 3 + MCPForUnity/Editor/Tools/ManageScript.cs | 2 +- .../Editor/Tools/ManageScriptableObject.cs | 14 +- MCPForUnity/Editor/Tools/ManageUI.cs | 74 +- .../Editor/Tools/Prefabs/ManagePrefabs.cs | 32 +- .../Tools/Profiler/Operations/CounterOps.cs | 5 + MCPForUnity/Editor/Tools/ReadConsole.cs | 2 +- MCPForUnity/Editor/Tools/UnityReflect.cs | 10 +- .../Editor/Tools/Vfx/ParticleControl.cs | 4 +- .../Components/AssetGen/McpAssetGenSection.cs | 9 +- .../AssetGen/McpAssetGenSection.uxml | 4 +- .../ClientConfig/McpClientConfigSection.cs | 11 +- .../ClientConfig/McpClientConfigSection.uxml | 4 +- .../Windows/Components/CompatDropdownField.cs | 180 ++ .../Components/CompatDropdownField.cs.meta | 11 + .../Resources/McpResourcesSection.cs | 4 +- .../Components/Tools/McpToolsSection.cs | 14 +- .../Windows/EditorPrefs/EditorPrefItem.uxml | 4 +- .../Windows/EditorPrefs/EditorPrefsWindow.cs | 3 +- .../EditorPrefs/EditorPrefsWindow.uxml | 4 +- .../Editor/Windows/MCPForUnityEditorWindow.cs | 2275 +++++++++-------- MCPForUnity/Editor/Windows/MCPSetupWindow.cs | 2 +- .../Serialization/UnityTypeConverters.cs | 8 +- MCPForUnity/package.json | 4 +- docs/UNITY_2020_3_COMPAT.md | 97 + 87 files changed, 1948 insertions(+), 1310 deletions(-) create mode 100644 MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs create mode 100644 MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs.meta create mode 100644 MCPForUnity/Editor/Windows/Components/CompatDropdownField.cs create mode 100644 MCPForUnity/Editor/Windows/Components/CompatDropdownField.cs.meta create mode 100644 docs/UNITY_2020_3_COMPAT.md diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 74fd836f7..a8f45d876 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -134,14 +134,14 @@ protected static bool IsBetaPackageSource(string packageSource) return true; // PyPI prerelease range: >=0.0.0a0 (used for prerelease package builds) - if (packageSource.Contains(">=0.0.0a0", StringComparison.OrdinalIgnoreCase)) + if (packageSource.IndexOf(">=0.0.0a0", StringComparison.OrdinalIgnoreCase) >= 0) return true; // Git-based beta references - if (packageSource.Contains("@beta", StringComparison.OrdinalIgnoreCase)) + if (packageSource.IndexOf("@beta", StringComparison.OrdinalIgnoreCase) >= 0) return true; - if (packageSource.Contains("-beta", StringComparison.OrdinalIgnoreCase)) + if (packageSource.IndexOf("-beta", StringComparison.OrdinalIgnoreCase) >= 0) return true; return false; diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs index be9db17f6..9bd18785e 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs @@ -182,7 +182,7 @@ protected string BuildAugmentedPath() if (additions.Length == 0) return null; // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); + return string.Join(Path.PathSeparator.ToString(), additions); } private string[] GetPathAdditions() diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs index 2c06219f5..3b38e1f04 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs @@ -180,7 +180,7 @@ protected string BuildAugmentedPath() if (additions.Length == 0) return null; // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); + return string.Join(Path.PathSeparator.ToString(), additions); } private string[] GetPathAdditions() diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs index 706e5030f..ff7d09a43 100644 --- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs +++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs @@ -245,7 +245,7 @@ protected string BuildAugmentedPath() if (additions.Length == 0) return null; // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); + return string.Join(Path.PathSeparator.ToString(), additions); } private string[] GetPathAdditions() diff --git a/MCPForUnity/Editor/External/Tommy.cs b/MCPForUnity/Editor/External/Tommy.cs index 22e83b816..f71820878 100644 --- a/MCPForUnity/Editor/External/Tommy.cs +++ b/MCPForUnity/Editor/External/Tommy.cs @@ -517,7 +517,7 @@ internal void WriteTo(TextWriter tw, string name, bool writeSectionName) if (collapsedItems.Count == 0) return; - var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true }); + var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } || n.Value is TomlArray { IsTableArray: true }); Comment?.AsComment(tw); @@ -539,7 +539,7 @@ internal void WriteTo(TextWriter tw, string name, bool writeSectionName) foreach (var collapsedItem in collapsedItems) { var key = collapsedItem.Key; - if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false }) + if (collapsedItem.Value is TomlArray { IsTableArray: true } || collapsedItem.Value is TomlTable { IsInline: false }) { if (!first) tw.WriteLine(); first = false; @@ -819,7 +819,7 @@ public TomlTable Parse() if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER) goto consume_character; - if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER) + if (c == TomlSyntax.COMMENT_SYMBOL || c == TomlSyntax.NEWLINE_CHARACTER) { currentState = ParseState.None; AdvanceLine(); @@ -1838,13 +1838,13 @@ internal static class TomlSyntax public const string POS_INF_VALUE = "+inf"; public const string NEG_INF_VALUE = "-inf"; - public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE; + public static bool IsBoolean(string s) => s == TRUE_VALUE || s == FALSE_VALUE; - public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE; + public static bool IsPosInf(string s) => s == INF_VALUE || s == POS_INF_VALUE; public static bool IsNegInf(string s) => s == NEG_INF_VALUE; - public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE; + public static bool IsNaN(string s) => s == NAN_VALUE || s == POS_NAN_VALUE || s == NEG_NAN_VALUE; public static bool IsInteger(string s) => IntegerPattern.IsMatch(s); @@ -1863,25 +1863,25 @@ public static bool IsIntegerWithBase(string s, out int numberBase) * A pattern to verify the integer value according to the TOML specification. */ public static readonly Regex IntegerPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled); + new Regex(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled); /** * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification. */ public static readonly Regex BasedIntegerPattern = - new(@"^0(?x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + new Regex(@"^0(?x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /** * A pattern to verify the float value according to the TOML specification. */ public static readonly Regex FloatPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$", + new Regex(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /** * A helper dictionary to map TOML base codes into the radii. */ - public static readonly Dictionary IntegerBases = new() + public static readonly Dictionary IntegerBases = new Dictionary() { ["x"] = 16, ["o"] = 8, @@ -1891,7 +1891,7 @@ public static bool IsIntegerWithBase(string s, out int numberBase) /** * A helper dictionary to map non-decimal bases to their TOML identifiers */ - public static readonly Dictionary BaseIdentifiers = new() + public static readonly Dictionary BaseIdentifiers = new Dictionary() { [2] = "b", [8] = "o", @@ -1962,29 +1962,29 @@ public static bool IsIntegerWithBase(string s, out int numberBase) public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; - public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; + public static bool IsQuoted(char c) => c == BASIC_STRING_SYMBOL || c == LITERAL_STRING_SYMBOL; - public static bool IsWhiteSpace(char c) => c is ' ' or '\t'; + public static bool IsWhiteSpace(char c) => c == ' ' || c == '\t'; - public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER; + public static bool IsNewLine(char c) => c == NEWLINE_CHARACTER || c == NEWLINE_CARRIAGE_RETURN_CHARACTER; public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER; public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c); public static bool IsBareKey(char c) => - c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-'; + (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; public static bool MustBeEscaped(char c, bool allowNewLines = false) { - var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f'; + var result = (c >= '\u0000' && c <= '\u0008') || c == '\u000b' || c == '\u000c' || (c >= '\u000e' && c <= '\u001f') || c == '\u007f'; if (!allowNewLines) - result |= c is >= '\u000a' and <= '\u000e'; + result |= c >= '\u000a' && c <= '\u000e'; return result; } public static bool IsValueSeparator(char c) => - c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL; + c == ITEM_SEPARATOR || c == ARRAY_END_SYMBOL || c == INLINE_TABLE_END_SYMBOL; #endregion } diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index 6daeab970..fcdd376b2 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -266,7 +266,7 @@ internal static string ResolveLocalServerPath(string path) } // If it looks like a PyPI package reference (no path separators), skip - if (!path.Contains('/') && !path.Contains('\\') && !path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) + if (!path.Contains("/") && !path.Contains("\\") && !path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) { return path; } @@ -658,11 +658,11 @@ private static bool IsSemVerPreRelease(string version) // Common semver prerelease indicators: // e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview" - return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) || - version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) || - version.Contains("-rc", StringComparison.OrdinalIgnoreCase) || - version.Contains("-preview", StringComparison.OrdinalIgnoreCase) || - version.Contains("-pre", StringComparison.OrdinalIgnoreCase); + return version.IndexOf("-beta", StringComparison.OrdinalIgnoreCase) >= 0 || + version.IndexOf("-alpha", StringComparison.OrdinalIgnoreCase) >= 0 || + version.IndexOf("-rc", StringComparison.OrdinalIgnoreCase) >= 0 || + version.IndexOf("-preview", StringComparison.OrdinalIgnoreCase) >= 0 || + version.IndexOf("-pre", StringComparison.OrdinalIgnoreCase) >= 0; } } } diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index 45ae5a39e..196f05466 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -236,7 +236,7 @@ private static void EnsureRmcpClientFeature(TomlTable root) { if (root == null) return; - if (!root.TryGetNode("features", out var featuresNode) || featuresNode is not TomlTable features) + if (!root.TryGetNode("features", out var featuresNode) || !(featuresNode is TomlTable features)) { features = new TomlTable(); root["features"] = features; diff --git a/MCPForUnity/Editor/Helpers/GameObjectLookup.cs b/MCPForUnity/Editor/Helpers/GameObjectLookup.cs index 7ee58efc1..b09b4cdbc 100644 --- a/MCPForUnity/Editor/Helpers/GameObjectLookup.cs +++ b/MCPForUnity/Editor/Helpers/GameObjectLookup.cs @@ -3,7 +3,11 @@ using System.Linq; using Newtonsoft.Json.Linq; using UnityEditor; +#if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; +#else +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEngine; using UnityEngine.SceneManagement; using MCPForUnity.Runtime.Helpers; diff --git a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs index 94e2f3be9..ce727fa35 100644 --- a/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs +++ b/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs @@ -333,7 +333,7 @@ private static string NormalizeBaseUrl(string value, string defaultUrl, bool rem // Strip trailing "/mcp" (case-insensitive) if provided. if (trimmed.EndsWith("/mcp", StringComparison.OrdinalIgnoreCase)) { - trimmed = trimmed[..^4]; + trimmed = trimmed.Substring(0, trimmed.Length - 4); } // For local scope, force 127.0.0.1 over "localhost". Windows getaddrinfo returns ::1 diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs index 61eccb13b..7db45f742 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -37,7 +37,7 @@ public static string WriteMcpConfiguration(string configPath, McpClient mcpClien } catch { } - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + JsonSerializerSettings jsonSettings = new JsonSerializerSettings() { Formatting = Formatting.Indented }; // Read existing config if it exists string existingJson = "{}"; diff --git a/MCPForUnity/Editor/Helpers/McpLogRecord.cs b/MCPForUnity/Editor/Helpers/McpLogRecord.cs index f126a47c2..84b30bc5d 100644 --- a/MCPForUnity/Editor/Helpers/McpLogRecord.cs +++ b/MCPForUnity/Editor/Helpers/McpLogRecord.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.IO; using MCPForUnity.Editor.Constants; using Newtonsoft.Json; @@ -15,7 +16,7 @@ internal static class McpLogRecord private static readonly string ErrorLogPath = Path.Combine(LogDir, "mcpError.log"); private const long MaxLogSizeBytes = 1024 * 1024; // 1 MB private static bool _sessionStarted; - private static readonly object _logLock = new(); + private static readonly object _logLock = new object(); private static volatile bool _isEnabledCached; [InitializeOnLoadMethod] @@ -106,7 +107,7 @@ private static void RotateIfNeeded(string path) var lines = File.ReadAllLines(path); var half = lines.Length / 2; - File.WriteAllLines(path, lines[half..]); + File.WriteAllLines(path, new ArraySegment(lines, half, lines.Length - half).ToArray()); } catch { diff --git a/MCPForUnity/Editor/Helpers/PortManager.cs b/MCPForUnity/Editor/Helpers/PortManager.cs index f9a2915c3..43ce861a7 100644 --- a/MCPForUnity/Editor/Helpers/PortManager.cs +++ b/MCPForUnity/Editor/Helpers/PortManager.cs @@ -336,7 +336,7 @@ private static string ComputeProjectHash(string input) { sb.Append(b.ToString("x2")); } - return sb.ToString()[..8]; // short, sufficient for filenames + return sb.ToString().Substring(0, 8); // short, sufficient for filenames } catch { diff --git a/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs b/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs new file mode 100644 index 000000000..876fd48f8 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs @@ -0,0 +1,43 @@ +using System.Diagnostics; + +namespace MCPForUnity.Editor.Helpers +{ + /// + /// Unity 2020.3 compatibility shim: ProcessStartInfo.ArgumentList was introduced in + /// .NET Core 2.1 / netstandard2.1. On Unity 2020.3 (netstandard2.0) we emulate it + /// with ProcessStartInfo.Arguments, quoting each argument. + /// + public static class ProcessArgumentListCompat + { + /// Append one argument (quoted if it contains whitespace) — replaces ArgumentList.Add. + public static ProcessStartInfo AddArg(this ProcessStartInfo psi, string arg) + { + if (string.IsNullOrEmpty(psi.Arguments)) + { + psi.Arguments = Quote(arg); + } + else + { + psi.Arguments += " " + Quote(arg); + } + return psi; + } + + private static string Quote(string arg) + { + if (string.IsNullOrEmpty(arg)) + { + return "\"\""; + } + + if (arg.IndexOfAny(new[] { ' ', '\t', '"' }) < 0) + { + return arg; + } + + // Escape embedded quotes the way Windows CreateProcess expects: + // backslashes before a quote are doubled, then the quote escaped. + return "\"" + arg.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + } +} diff --git a/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs.meta b/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs.meta new file mode 100644 index 000000000..3083dadd8 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 19b085f18fec119419363e70edc91c9f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs b/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs index 34a53916f..023754dbb 100644 --- a/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs +++ b/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs @@ -113,7 +113,7 @@ private static string ComputeProjectName(string dataPath) projectPath = projectPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) { - projectPath = projectPath[..^6].TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } string name = Path.GetFileName(projectPath); diff --git a/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs b/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs index 7ed1bbe39..bc1c39888 100644 --- a/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs +++ b/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs @@ -18,8 +18,8 @@ namespace MCPForUnity.Editor.Helpers /// public static class UnityTypeResolver { - private static readonly Dictionary CacheByFqn = new(StringComparer.Ordinal); - private static readonly Dictionary CacheByName = new(StringComparer.Ordinal); + private static readonly Dictionary CacheByFqn = new Dictionary(StringComparer.Ordinal); + private static readonly Dictionary CacheByName = new Dictionary(StringComparer.Ordinal); /// /// Resolves a type by name, with optional base type constraint. @@ -150,7 +150,7 @@ private static void Cache(Type t) private static List FindCandidates(string query, Type requiredBaseType) { - bool isShort = !query.Contains('.'); + bool isShort = !query.Contains("."); var loaded = UnityAssembliesCompat.GetLoadedAssemblies(); #if UNITY_EDITOR @@ -166,9 +166,11 @@ private static List FindCandidates(string query, Type requiredBaseType) var editorAsms = Array.Empty(); #endif - Func match = isShort - ? (t => t.Name.Equals(query, StringComparison.Ordinal)) - : (t => t.FullName?.Equals(query, StringComparison.Ordinal) ?? false); + Func match; + if (isShort) + match = t => t.Name.Equals(query, StringComparison.Ordinal); + else + match = t => t.FullName?.Equals(query, StringComparison.Ordinal) ?? false; var fromPlayer = playerAsms.SelectMany(SafeGetTypes) .Where(t => PassesConstraint(t, requiredBaseType)) diff --git a/MCPForUnity/Editor/Helpers/VectorParsing.cs b/MCPForUnity/Editor/Helpers/VectorParsing.cs index 0e81cca88..f0e27a8b2 100644 --- a/MCPForUnity/Editor/Helpers/VectorParsing.cs +++ b/MCPForUnity/Editor/Helpers/VectorParsing.cs @@ -523,7 +523,7 @@ public static bool ValidateAnimationCurveFormat(JToken valueToken, out string me for (int i = 0; i < keysArray.Count; i++) { var keyToken = keysArray[i]; - if (keyToken is not JObject keyObj) + if (!(keyToken is JObject keyObj)) { message = $"Keyframe at index {i} must be an object with 'time' and 'value'."; return false; diff --git a/MCPForUnity/Editor/Models/McpClient.cs b/MCPForUnity/Editor/Models/McpClient.cs index 85b30c2ce..932562936 100644 --- a/MCPForUnity/Editor/Models/McpClient.cs +++ b/MCPForUnity/Editor/Models/McpClient.cs @@ -22,7 +22,7 @@ public class McpClient public string ServerContainerKey; // Top-level JSON container key for servers (null => "mcpServers"; Kilo => "mcp") public string SchemaUrl; // Optional root "$schema" URL written into the config (e.g. Kilo's kilo.jsonc) public string HttpUrlProperty = "url"; // The property name for the HTTP URL in the config - public Dictionary DefaultUnityFields = new(); + public Dictionary DefaultUnityFields = new Dictionary(); // Helper method to convert the enum to a display string public string GetStatusDisplayString() diff --git a/MCPForUnity/Editor/Resources/Editor/GetPrefabStage.cs b/MCPForUnity/Editor/Resources/Editor/GetPrefabStage.cs index 28bd0a06e..810ff4b61 100644 --- a/MCPForUnity/Editor/Resources/Editor/GetPrefabStage.cs +++ b/MCPForUnity/Editor/Resources/Editor/GetPrefabStage.cs @@ -1,7 +1,11 @@ using System; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; +#if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; +#else +using UnityEditor.Experimental.SceneManagement; +#endif namespace MCPForUnity.Editor.Resources.Editor { diff --git a/MCPForUnity/Editor/Security/SecureKeyStore/EncryptedFileKeyStore.cs b/MCPForUnity/Editor/Security/SecureKeyStore/EncryptedFileKeyStore.cs index 3774b3731..c4779ef95 100644 --- a/MCPForUnity/Editor/Security/SecureKeyStore/EncryptedFileKeyStore.cs +++ b/MCPForUnity/Editor/Security/SecureKeyStore/EncryptedFileKeyStore.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text; using UnityEngine; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Security { @@ -92,7 +93,7 @@ private void DeriveKeys(out byte[] encKey, out byte[] macKey) byte[] master = LoadOrCreate(Path.Combine(_dir, "secret.bin"), 32); byte[] salt = LoadOrCreate(Path.Combine(_dir, "salt.bin"), 16); string password = Convert.ToBase64String(master) + "|" + MachineId(); - using (var kdf = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256)) + using (var kdf = new Rfc2898DeriveBytes(password, salt, Iterations)) { byte[] material = kdf.GetBytes(64); encKey = new byte[32]; @@ -210,8 +211,8 @@ private static void TryChmod(string path, string mode) RedirectStandardError = true, RedirectStandardOutput = true, }; - psi.ArgumentList.Add(mode); - psi.ArgumentList.Add(path); + psi.AddArg(mode); + psi.AddArg(path); using (var p = System.Diagnostics.Process.Start(psi)) p?.WaitForExit(2000); } catch { /* hardening is best-effort */ } diff --git a/MCPForUnity/Editor/Security/SecureKeyStore/LinuxSecretToolKeyStore.cs b/MCPForUnity/Editor/Security/SecureKeyStore/LinuxSecretToolKeyStore.cs index 96710e8ad..b14de1100 100644 --- a/MCPForUnity/Editor/Security/SecureKeyStore/LinuxSecretToolKeyStore.cs +++ b/MCPForUnity/Editor/Security/SecureKeyStore/LinuxSecretToolKeyStore.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Security { @@ -17,7 +18,7 @@ internal static bool IsAvailable() try { var psi = NewPsi(); - psi.ArgumentList.Add("--version"); + psi.AddArg("--version"); using (var p = Process.Start(psi)) { p.WaitForExit(2000); @@ -36,9 +37,9 @@ public bool TryGet(string providerId, out string apiKey) try { var psi = NewPsi(); - psi.ArgumentList.Add("lookup"); - psi.ArgumentList.Add("service"); psi.ArgumentList.Add(Service); - psi.ArgumentList.Add("account"); psi.ArgumentList.Add(providerId); + psi.AddArg("lookup"); + psi.AddArg("service"); psi.AddArg(Service); + psi.AddArg("account"); psi.AddArg(providerId); using (var p = Process.Start(psi)) { string outp = p.StandardOutput.ReadToEnd(); @@ -58,10 +59,10 @@ public void Set(string providerId, string apiKey) try { var psi = NewPsi(redirectIn: true); - psi.ArgumentList.Add("store"); - psi.ArgumentList.Add("--label=MCPForUnity AssetGen"); - psi.ArgumentList.Add("service"); psi.ArgumentList.Add(Service); - psi.ArgumentList.Add("account"); psi.ArgumentList.Add(providerId); + psi.AddArg("store"); + psi.AddArg("--label=MCPForUnity AssetGen"); + psi.AddArg("service"); psi.AddArg(Service); + psi.AddArg("account"); psi.AddArg(providerId); using (var p = Process.Start(psi)) { p.StandardInput.Write(apiKey); @@ -78,9 +79,9 @@ public void Delete(string providerId) try { var psi = NewPsi(); - psi.ArgumentList.Add("clear"); - psi.ArgumentList.Add("service"); psi.ArgumentList.Add(Service); - psi.ArgumentList.Add("account"); psi.ArgumentList.Add(providerId); + psi.AddArg("clear"); + psi.AddArg("service"); psi.AddArg(Service); + psi.AddArg("account"); psi.AddArg(providerId); using (var p = Process.Start(psi)) p.WaitForExit(5000); } catch { /* best effort */ } @@ -96,7 +97,7 @@ private static ProcessStartInfo NewPsi(bool redirectIn = false) RedirectStandardError = true, RedirectStandardInput = redirectIn, }; - psi.ArgumentList.Add("secret-tool"); + psi.AddArg("secret-tool"); return psi; } } diff --git a/MCPForUnity/Editor/Security/SecureKeyStore/MacKeychainKeyStore.cs b/MCPForUnity/Editor/Security/SecureKeyStore/MacKeychainKeyStore.cs index 9cf86249b..306209e30 100644 --- a/MCPForUnity/Editor/Security/SecureKeyStore/MacKeychainKeyStore.cs +++ b/MCPForUnity/Editor/Security/SecureKeyStore/MacKeychainKeyStore.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Security { @@ -49,7 +50,7 @@ private static (int code, string stdout, string stderr) Run(params string[] args RedirectStandardOutput = true, RedirectStandardError = true, }; - foreach (string a in args) psi.ArgumentList.Add(a); + foreach (string a in args) psi.AddArg(a); using (var p = Process.Start(psi)) { string outp = p.StandardOutput.ReadToEnd(); diff --git a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs index e9fa9e674..d5f0a24a1 100644 --- a/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs +++ b/MCPForUnity/Editor/Services/AssetGen/AssetGenJobManager.cs @@ -57,9 +57,9 @@ public static class AssetGenJobManager internal static double PollIntervalSeconds = 3.0; internal static double TimeoutSeconds = 600.0; - private static readonly Dictionary Jobs = new(); - private static readonly Dictionary Runners = new(); - private static readonly List _tickIds = new(); + private static readonly Dictionary Jobs = new Dictionary(); + private static readonly Dictionary Runners = new Dictionary(); + private static readonly List _tickIds = new List(); private static bool _ticking; static AssetGenJobManager() @@ -245,7 +245,7 @@ private sealed class Runner public string Name; public string Subfolder; - public CancellationTokenSource Cts = new(); + public CancellationTokenSource Cts = new CancellationTokenSource(); public RunnerPhase Phase = RunnerPhase.Submit; public double StartedAt; public double NextPollAt; @@ -441,20 +441,20 @@ private static void Advance(Runner r) // provider-controlled result URL (OverrideExt), so a rogue provider could otherwise land a // .cs/.asmdef/.meta/.asset under Assets/ and get it compiled/imported on Refresh — Editor // RCE. Anything outside these sets is rejected. Mirrors ModelImportPipeline's allowlist style. - private static readonly HashSet AudioAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet AudioAllowedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { "wav", "mp3", "ogg", "aiff", "aif", "flac", }; - private static readonly HashSet ImageAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet ImageAllowedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { "png", "jpg", "jpeg", "exr", "tga", "psd", "tiff", "webp", "gif", "bmp", }; - private static readonly HashSet ModelAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet ModelAllowedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { "glb", "gltf", "fbx", "obj", "usd", "usdz", "dae", "ply", "stl", "zip", }; // Fail closed: an unexpected kind allows nothing, so the RCE boundary never opens by default. - private static readonly HashSet NoAllowedExtensions = new(StringComparer.OrdinalIgnoreCase); + private static readonly HashSet NoAllowedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase); /// /// Whether (no leading dot) is an allowed result extension for the diff --git a/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs b/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs index 02f1ab035..9c77c1df0 100644 --- a/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs +++ b/MCPForUnity/Editor/Services/AssetGen/Import/ModelImportPipeline.cs @@ -19,7 +19,7 @@ public static class ModelImportPipeline // Inert asset types permitted out of an UNTRUSTED provider archive (Sketchfab et al.). // Anything else — scripts, assemblies, asmdefs — is skipped on extraction so it can never // compile or load inside the Editor. See SafeZipExtractor for the enforcement. - private static readonly HashSet ArchiveAllowedExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet ArchiveAllowedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".gltf", ".glb", ".bin", ".fbx", ".obj", ".mtl", ".png", ".jpg", ".jpeg", ".tga", ".bmp", ".tif", ".tiff", ".webp", ".exr", ".hdr", diff --git a/MCPForUnity/Editor/Services/AssetGen/Providers/LocalImage.cs b/MCPForUnity/Editor/Services/AssetGen/Providers/LocalImage.cs index ce81358f5..82b629813 100644 --- a/MCPForUnity/Editor/Services/AssetGen/Providers/LocalImage.cs +++ b/MCPForUnity/Editor/Services/AssetGen/Providers/LocalImage.cs @@ -13,7 +13,7 @@ namespace MCPForUnity.Editor.Services.AssetGen.Providers internal static class LocalImage { // Extensions that can be inlined as a data URI for provider image input. - private static readonly HashSet SupportedExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet SupportedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".webp", ".gif" }; /// diff --git a/MCPForUnity/Editor/Services/EditorStateCache.cs b/MCPForUnity/Editor/Services/EditorStateCache.cs index d02b26528..0be7a67bb 100644 --- a/MCPForUnity/Editor/Services/EditorStateCache.cs +++ b/MCPForUnity/Editor/Services/EditorStateCache.cs @@ -17,7 +17,7 @@ namespace MCPForUnity.Editor.Services [InitializeOnLoad] internal static class EditorStateCache { - private static readonly object LockObj = new(); + private static readonly object LockObj = new object(); private static long _sequence; private static long _observedUnixMs; diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs b/MCPForUnity/Editor/Services/IClientConfigurationService.cs index 6172e8fb3..748445a38 100644 --- a/MCPForUnity/Editor/Services/IClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs @@ -56,7 +56,7 @@ public class ClientConfigurationSummary /// /// Detailed messages for each client /// - public System.Collections.Generic.List Messages { get; set; } = new(); + public System.Collections.Generic.List Messages { get; set; } = new System.Collections.Generic.List(); /// /// Gets a human-readable summary message diff --git a/MCPForUnity/Editor/Services/PackageJobManager.cs b/MCPForUnity/Editor/Services/PackageJobManager.cs index d2cba494a..83afd07d3 100644 --- a/MCPForUnity/Editor/Services/PackageJobManager.cs +++ b/MCPForUnity/Editor/Services/PackageJobManager.cs @@ -31,8 +31,8 @@ internal static class PackageJobManager private const int MaxJobsToKeep = 10; private const long DomainReloadTimeoutMs = 120_000; - private static readonly object LockObj = new(); - private static readonly Dictionary Jobs = new(); + private static readonly object LockObj = new object(); + private static readonly Dictionary Jobs = new Dictionary(); static PackageJobManager() { @@ -128,7 +128,7 @@ internal static void TryRecoverJob(PackageJob job, long nowMs) try { string packageName = ExtractPackageName(job.Package); - var allPackages = PackageInfo.GetAllRegisteredPackages(); + var allPackages = GetRegisteredPackages(); var info = FindPackageInfo(allPackages, packageName, job.Package); if (job.Operation == "add" || job.Operation == "embed") @@ -179,6 +179,28 @@ internal static void TryRecoverJob(PackageJob job, long nowMs) /// /// Find a PackageInfo by name, falling back to packageId or git/local source for non-standard identifiers. /// + + + private static PackageInfo[] GetRegisteredPackages() + { +#if UNITY_2021_2_OR_NEWER + return PackageInfo.GetAllRegisteredPackages(); +#else + var request = UnityEditor.PackageManager.Client.List(true); + while (!request.IsCompleted) + { + System.Threading.Thread.Sleep(10); + } + if (request.IsCompleted && request.Status == UnityEditor.PackageManager.StatusCode.Success) + { + var list = new System.Collections.Generic.List(); + foreach (var pi in request.Result) list.Add(pi); + return list.ToArray(); + } + return new PackageInfo[0]; +#endif + } + private static PackageInfo FindPackageInfo(PackageInfo[] allPackages, string packageName, string originalIdentifier) { // Direct name match (handles normal com.company.package identifiers) diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs b/MCPForUnity/Editor/Services/PackageUpdateService.cs index 8c441d9b9..29d204e1d 100644 --- a/MCPForUnity/Editor/Services/PackageUpdateService.cs +++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs @@ -281,7 +281,7 @@ private static bool IsPreReleaseVersion(string version) return AssetPathUtility.IsPreReleaseVersion(); } - return version.IndexOf('-', StringComparison.Ordinal) >= 0; + return version.IndexOf("-", StringComparison.Ordinal) >= 0; } /// diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs index c233d8680..d5acbf1f5 100644 --- a/MCPForUnity/Editor/Services/PathResolverService.cs +++ b/MCPForUnity/Editor/Services/PathResolverService.cs @@ -187,7 +187,7 @@ public bool TryValidateUvxExecutable(string uvxPath, out string version) try { // Check if the path is just a command name (no directory separator) - bool isBareCommand = !uvxPath.Contains('/') && !uvxPath.Contains('\\'); + bool isBareCommand = !uvxPath.Contains("/") && !uvxPath.Contains("\\"); if (isBareCommand) { diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs b/MCPForUnity/Editor/Services/TestJobManager.cs index bdf626036..2c07514e7 100644 --- a/MCPForUnity/Editor/Services/TestJobManager.cs +++ b/MCPForUnity/Editor/Services/TestJobManager.cs @@ -60,8 +60,8 @@ internal static class TestJobManager private const string SessionKeyJobs = "MCPForUnity.TestJobsV1"; private const string SessionKeyCurrentJobId = "MCPForUnity.CurrentTestJobIdV1"; - private static readonly object LockObj = new(); - private static readonly Dictionary Jobs = new(); + private static readonly object LockObj = new object(); + private static readonly Dictionary Jobs = new Dictionary(); private static string _currentJobId; private static long _lastPersistUnixMs; diff --git a/MCPForUnity/Editor/Services/TestRunStatus.cs b/MCPForUnity/Editor/Services/TestRunStatus.cs index da3ae6c26..c0db7f08d 100644 --- a/MCPForUnity/Editor/Services/TestRunStatus.cs +++ b/MCPForUnity/Editor/Services/TestRunStatus.cs @@ -9,7 +9,7 @@ namespace MCPForUnity.Editor.Services /// internal static class TestRunStatus { - private static readonly object LockObj = new(); + private static readonly object LockObj = new object(); private static bool _isRunning; private static TestMode? _mode; diff --git a/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs b/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs index becc3bd88..1df42d7d2 100644 --- a/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs +++ b/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs @@ -62,8 +62,8 @@ public void TrySetCanceled() } } - private static readonly Dictionary Pending = new(); - private static readonly object PendingLock = new(); + private static readonly Dictionary Pending = new Dictionary(); + private static readonly object PendingLock = new object(); private static bool updateHooked; private static bool initialised; @@ -303,7 +303,7 @@ private static void ProcessCommand(string id, PendingCommand pending) { status = "error", error = "Invalid JSON format", - receivedText = commandText.Length > 50 ? commandText[..50] + "..." : commandText + receivedText = commandText.Length > 50 ? commandText.Substring(0, 50) + "..." : commandText }; pending.TrySetResult(JsonConvert.SerializeObject(invalidJsonResponse)); RemovePending(id, pending); @@ -381,7 +381,7 @@ private static void ProcessCommand(string id, PendingCommand pending) logStatus = "ERROR"; logError = t.Exception?.InnerException?.Message; } - else if (t.IsCompletedSuccessfully && t.Result != null) + else if (t.Status == TaskStatus.RanToCompletion && t.Result != null) { try { @@ -429,8 +429,9 @@ private static void CancelPending(string id, CancellationToken token) PendingCommand pending = null; lock (PendingLock) { - if (Pending.Remove(id, out pending)) + if (Pending.TryGetValue(id, out pending)) { + Pending.Remove(id); UnhookUpdateIfIdle(); } } diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index a6a0559d0..1268ab339 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -34,10 +34,10 @@ public static class StdioBridgeHost { private static TcpListener listener; private static bool isRunning = false; - private static readonly object lockObj = new(); - private static readonly object startStopLock = new(); - private static readonly object clientsLock = new(); - private static readonly HashSet activeClients = new(); + private static readonly object lockObj = new object(); + private static readonly object startStopLock = new object(); + private static readonly object clientsLock = new object(); + private static readonly HashSet activeClients = new HashSet(); private static CancellationTokenSource cts; private static Task listenerTask; private static int processingCommands = 0; @@ -53,7 +53,7 @@ public static class StdioBridgeHost // stale leftover from an abandoned retry and a fresh window is started (#1173). private const double PortBusyStaleResetSeconds = 60.0; private static int heartbeatSeq = 0; - private static Dictionary commandQueue = new(); + private static Dictionary commandQueue = new Dictionary(); private static int mainThreadId; private static int currentUnityPort = 6400; private static bool isAutoConnectMode = false; @@ -116,7 +116,7 @@ public static bool FolderExists(string path) string fullPath = Path.Combine( Application.dataPath, - path.StartsWith("Assets/") ? path[7..] : path + path.StartsWith("Assets/") ? path.Substring(7) : path ); return Directory.Exists(fullPath); } @@ -896,7 +896,7 @@ private static void ProcessCommands() status = "error", error = "Invalid JSON format", receivedText = commandText.Length > 50 - ? commandText[..50] + "..." + ? commandText.Substring(0, 50) + "..." : commandText, }; tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); @@ -940,7 +940,7 @@ async void Runner() status = "error", error = ex.Message, receivedText = payload?.Length > 50 - ? payload[..50] + "..." + ? payload.Substring(0, 50) + "..." : payload, }; completionSource.TrySetResult(JsonConvert.SerializeObject(response)); @@ -1109,7 +1109,7 @@ private static string ComputeProjectHash(string input) { sb.Append(b.ToString("x2")); } - return sb.ToString()[..8]; + return sb.ToString().Substring(0, 8); } catch { diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs index 8aaf2249e..7f5828ee1 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using System.Net.WebSockets; @@ -45,7 +44,7 @@ public class WebSocketTransportClient : IMcpTransportClient, IDisposable private CancellationTokenSource _connectionCts; private Task _receiveTask; private Task _keepAliveTask; - private readonly SemaphoreSlim _sendLock = new(1, 1); + private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); private Uri _endpointUri; private string _sessionId; @@ -415,7 +414,7 @@ private async Task ReceiveMessageAsync(CancellationToken token) return null; } - byte[] rentedBuffer = System.Buffers.ArrayPool.Shared.Rent(8192); + var rentedBuffer = new byte[8192]; var buffer = new ArraySegment(rentedBuffer); using var ms = new MemoryStream(8192); @@ -451,7 +450,6 @@ private async Task ReceiveMessageAsync(CancellationToken token) } finally { - System.Buffers.ArrayPool.Shared.Return(rentedBuffer); } } diff --git a/MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs b/MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs index 5d6709368..329e912b9 100644 --- a/MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs +++ b/MCPForUnity/Editor/Setup/McpForUnitySkillInstaller.cs @@ -25,8 +25,8 @@ public class McpForUnitySkillInstaller : EditorWindow private string _installDir; private Vector2 _scroll; private volatile bool _isRunning; - private readonly ConcurrentQueue _pendingLogs = new(); - private readonly StringBuilder _logBuilder = new(4096); + private readonly ConcurrentQueue _pendingLogs = new ConcurrentQueue(); + private readonly StringBuilder _logBuilder = new StringBuilder(4096); public static void OpenWindow() { diff --git a/MCPForUnity/Editor/Setup/SkillSyncService.cs b/MCPForUnity/Editor/Setup/SkillSyncService.cs index 745d05856..1eeb73dfc 100644 --- a/MCPForUnity/Editor/Setup/SkillSyncService.cs +++ b/MCPForUnity/Editor/Setup/SkillSyncService.cs @@ -513,7 +513,7 @@ internal static Dictionary ListFiles(string root) var normalizedRoot = Path.GetFullPath(root); foreach (var filePath in Directory.GetFiles(normalizedRoot, "*", SearchOption.AllDirectories)) { - var relativePath = Path.GetRelativePath(normalizedRoot, filePath).Replace('\\', '/'); + var relativePath = MakeRelativePath(normalizedRoot, filePath).Replace('\\', '/'); if (string.Equals(relativePath, SyncOwnershipMarker, StringComparison.OrdinalIgnoreCase)) { continue; @@ -808,9 +808,19 @@ internal sealed class GitHubTreeEntry internal sealed class SyncPlan { - public List Added { get; } = new(); - public List Updated { get; } = new(); - public List Deleted { get; } = new(); + public List Added { get; } = new List(); + public List Updated { get; } = new List(); + public List Deleted { get; } = new List(); + } + + private static string MakeRelativePath(string root, string fullPath) + { + string rootNorm = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + if (fullPath.StartsWith(rootNorm, StringComparison.OrdinalIgnoreCase)) + { + return fullPath.Substring(rootNorm.Length); + } + return fullPath; } } } diff --git a/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs b/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs index b091dce37..8f131e00a 100644 --- a/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs +++ b/MCPForUnity/Editor/Tools/Animation/ClipCreate.cs @@ -251,7 +251,7 @@ public static object SetVectorCurve(JObject @params) string relativePath = @params["relativePath"]?.ToString() ?? ""; JToken keysToken = @params["keys"]; - if (keysToken == null || keysToken is not JArray keysArray || keysArray.Count == 0) + if (keysToken == null || !(keysToken is JArray keysArray) || keysArray.Count == 0) return new { success = false, message = "'keys' is required. Use [{\"time\":0,\"value\":[0,1,0]},...]" }; // Map property group to axis suffixes @@ -281,12 +281,12 @@ public static object SetVectorCurve(JObject @params) foreach (var item in keysArray) { - if (item is not JObject keyObj) + if (!(item is JObject keyObj)) return new { success = false, message = "Each key must be an object with 'time' and 'value' (Vector3 array)" }; float time = keyObj["time"]?.ToObject() ?? 0f; JToken valueToken = keyObj["value"]; - if (valueToken is not JArray valArray || valArray.Count < 3) + if (!(valueToken is JArray valArray) || valArray.Count < 3) return new { success = false, message = $"Key at time {time}: 'value' must be a 3-element array [x, y, z]" }; float vx = valArray[0].ToObject(); diff --git a/MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs b/MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs index 328c18d78..76e786188 100644 --- a/MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs +++ b/MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs @@ -180,7 +180,7 @@ public static object AddTransition(JObject @params) { foreach (var condItem in conditionsArray) { - if (condItem is not JObject condObj) continue; + if (!(condItem is JObject condObj)) continue; string paramName = condObj["parameter"]?.ToString(); if (string.IsNullOrEmpty(paramName)) continue; diff --git a/MCPForUnity/Editor/Tools/AssetGen/AssetGenToolHelpers.cs b/MCPForUnity/Editor/Tools/AssetGen/AssetGenToolHelpers.cs index e2955f82a..eb5565d5d 100644 --- a/MCPForUnity/Editor/Tools/AssetGen/AssetGenToolHelpers.cs +++ b/MCPForUnity/Editor/Tools/AssetGen/AssetGenToolHelpers.cs @@ -47,7 +47,7 @@ public static object Cancel(ToolParams p) string jobId = p.Get("job_id"); if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for cancel."); return AssetGenJobManager.Cancel(jobId) - ? new SuccessResponse($"Cancel requested for job '{jobId}'.") + ? (IMcpResponse)new SuccessResponse($"Cancel requested for job '{jobId}'.") : new ErrorResponse($"No cancelable job found with ID '{jobId}'."); } diff --git a/MCPForUnity/Editor/Tools/AssetGen/ImportModel.cs b/MCPForUnity/Editor/Tools/AssetGen/ImportModel.cs index 0f5103692..266a84310 100644 --- a/MCPForUnity/Editor/Tools/AssetGen/ImportModel.cs +++ b/MCPForUnity/Editor/Tools/AssetGen/ImportModel.cs @@ -144,7 +144,7 @@ private static object Cancel(ToolParams p) string jobId = p.Get("job_id"); if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for cancel."); return AssetGenJobManager.Cancel(jobId) - ? new SuccessResponse($"Cancel requested for job '{jobId}'.") + ? (IMcpResponse)new SuccessResponse($"Cancel requested for job '{jobId}'.") : new ErrorResponse($"No cancelable job found with ID '{jobId}'."); } diff --git a/MCPForUnity/Editor/Tools/BatchExecute.cs b/MCPForUnity/Editor/Tools/BatchExecute.cs index aca4a1994..ff49bbf31 100644 --- a/MCPForUnity/Editor/Tools/BatchExecute.cs +++ b/MCPForUnity/Editor/Tools/BatchExecute.cs @@ -28,7 +28,7 @@ public static class BatchExecute internal static int GetMaxCommandsPerBatch() { int configured = EditorPrefs.GetInt(EditorPrefKeys.BatchExecuteMaxCommands, DefaultMaxCommandsPerBatch); - return Math.Clamp(configured, 1, AbsoluteMaxCommandsPerBatch); + return UnityEngine.Mathf.Clamp(configured, 1, AbsoluteMaxCommandsPerBatch); } public static async Task HandleCommand(JObject @params) @@ -67,7 +67,7 @@ public static async Task HandleCommand(JObject @params) foreach (var token in commandsToken) { - if (token is not JObject commandObj) + if (!(token is JObject commandObj)) { invocationFailureCount++; anyCommandFailed = true; @@ -177,7 +177,7 @@ public static async Task HandleCommand(JObject @params) }; return overallSuccess - ? new SuccessResponse("Batch execution completed.", data) + ? (IMcpResponse)new SuccessResponse("Batch execution completed.", data) : new ErrorResponse("One or more commands failed.", data); } diff --git a/MCPForUnity/Editor/Tools/Build/BuildJob.cs b/MCPForUnity/Editor/Tools/Build/BuildJob.cs index 74476136b..1424b30cd 100644 --- a/MCPForUnity/Editor/Tools/Build/BuildJob.cs +++ b/MCPForUnity/Editor/Tools/Build/BuildJob.cs @@ -74,7 +74,7 @@ public class BatchJob { public string JobId { get; } public BuildJobState State { get; set; } = BuildJobState.Pending; - public List Children { get; } = new(); + public List Children { get; } = new List(); public int CurrentIndex { get; set; } = -1; public BatchJob(string jobId) @@ -120,8 +120,8 @@ public object ToStatusResponse() /// public static class BuildJobStore { - private static readonly Dictionary _buildJobs = new(); - private static readonly Dictionary _batchJobs = new(); + private static readonly Dictionary _buildJobs = new Dictionary(); + private static readonly Dictionary _batchJobs = new Dictionary(); private static BuildJob _lastCompletedJob; public static string CreateJobId() => $"build-{Guid.NewGuid():N}".Substring(0, 16); diff --git a/MCPForUnity/Editor/Tools/Build/BuildRunner.cs b/MCPForUnity/Editor/Tools/Build/BuildRunner.cs index baed78da6..82161914d 100644 --- a/MCPForUnity/Editor/Tools/Build/BuildRunner.cs +++ b/MCPForUnity/Editor/Tools/Build/BuildRunner.cs @@ -69,7 +69,9 @@ public static BuildPlayerOptions CreateBuildOptions( || target == BuildTarget.StandaloneOSX || target == BuildTarget.StandaloneLinux64) { +#if UNITY_2021_2_OR_NEWER options.subtarget = subtarget; +#endif } return options; @@ -87,7 +89,11 @@ public static BuildOptions ParseBuildOptions(string[] optionNames, bool developm { switch (name.ToLowerInvariant()) { - case "clean_build": opts |= BuildOptions.CleanBuildCache; break; + case "clean_build": +#if UNITY_2021_2_OR_NEWER + opts |= BuildOptions.CleanBuildCache; +#endif + break; case "auto_run": opts |= BuildOptions.AutoRunPlayer; break; case "deep_profiling": opts |= BuildOptions.EnableDeepProfilingSupport; break; case "compress_lz4": opts |= BuildOptions.CompressWithLz4; break; diff --git a/MCPForUnity/Editor/Tools/Build/BuildSettingsHelper.cs b/MCPForUnity/Editor/Tools/Build/BuildSettingsHelper.cs index fae16d37c..75dadabae 100644 --- a/MCPForUnity/Editor/Tools/Build/BuildSettingsHelper.cs +++ b/MCPForUnity/Editor/Tools/Build/BuildSettingsHelper.cs @@ -7,7 +7,7 @@ namespace MCPForUnity.Editor.Tools.Build { public static class BuildSettingsHelper { - public static object ReadProperty(string property, NamedBuildTarget namedTarget) + public static object ReadProperty(string property, BuildTargetGroup namedTarget) { switch (property.ToLowerInvariant()) { @@ -23,7 +23,7 @@ public static object ReadProperty(string property, NamedBuildTarget namedTarget) var backend = PlayerSettings.GetScriptingBackend(namedTarget); return new { property, value = backend == ScriptingImplementation.IL2CPP ? "il2cpp" : "mono" }; case "defines": - return new { property, value = PlayerSettings.GetScriptingDefineSymbols(namedTarget) }; + return new { property, value = PlayerSettings.GetScriptingDefineSymbolsForGroup(namedTarget) }; case "architecture": var arch = PlayerSettings.GetArchitecture(namedTarget); string archName = arch switch { 0 => "x86_64", 1 => "arm64", 2 => "universal", _ => "unknown" }; @@ -33,7 +33,7 @@ public static object ReadProperty(string property, NamedBuildTarget namedTarget) } } - public static string WriteProperty(string property, string value, NamedBuildTarget namedTarget) + public static string WriteProperty(string property, string value, BuildTargetGroup namedTarget) { try { @@ -61,12 +61,14 @@ public static string WriteProperty(string property, string value, NamedBuildTarg PlayerSettings.SetScriptingBackend(namedTarget, impl); return null; case "defines": - PlayerSettings.SetScriptingDefineSymbols(namedTarget, value); + PlayerSettings.SetScriptingDefineSymbolsForGroup(namedTarget, value); return null; case "architecture": int arch = value.ToLowerInvariant() switch { - "x86_64" or "none" or "default" => 0, + "x86_64" => 0, + "none" => 0, + "default" => 0, "arm64" => 1, "universal" => 2, _ => -1 diff --git a/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs b/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs index 43383c672..fbfeffe1a 100644 --- a/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs +++ b/MCPForUnity/Editor/Tools/Build/BuildTargetMapping.cs @@ -63,12 +63,12 @@ public static BuildTargetGroup GetTargetGroup(BuildTarget target) } } - public static NamedBuildTarget GetNamedBuildTarget(BuildTarget target) + public static BuildTargetGroup GetNamedBuildTarget(BuildTarget target) { - return NamedBuildTarget.FromBuildTargetGroup(GetTargetGroup(target)); + return GetTargetGroup(target); } - public static string TryResolveNamedBuildTarget(string name, out NamedBuildTarget namedTarget) + public static string TryResolveNamedBuildTarget(string name, out BuildTargetGroup namedTarget) { if (!TryResolveBuildTarget(name, out var buildTarget)) { @@ -85,7 +85,7 @@ public static string TryResolveNamedBuildTarget(string name, out NamedBuildTarge : $"Build target group could not be resolved for target '{buildTarget}'."; } - namedTarget = NamedBuildTarget.FromBuildTargetGroup(targetGroup); + namedTarget = targetGroup; return null; } @@ -154,12 +154,17 @@ public static string GetDefaultOutputPath(BuildTarget target, string productName public static int ResolveSubtarget(string subtarget) { +#if UNITY_2021_2_OR_NEWER if (string.IsNullOrEmpty(subtarget)) return (int)StandaloneBuildSubtarget.Player; string lower = subtarget.ToLowerInvariant(); if (lower == "server") return (int)StandaloneBuildSubtarget.Server; return (int)StandaloneBuildSubtarget.Player; +#else + // Unity 2020.3 has no StandaloneBuildSubtarget API; only Player subtarget is supported. + return 0; +#endif } } } diff --git a/MCPForUnity/Editor/Tools/Cameras/CameraCreate.cs b/MCPForUnity/Editor/Tools/Cameras/CameraCreate.cs index d01e2ca84..f0920af4d 100644 --- a/MCPForUnity/Editor/Tools/Cameras/CameraCreate.cs +++ b/MCPForUnity/Editor/Tools/Cameras/CameraCreate.cs @@ -11,7 +11,7 @@ namespace MCPForUnity.Editor.Tools.Cameras { internal static class CameraCreate { - private static readonly Dictionary Presets = new(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary Presets = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["follow"] = ("CinemachineFollow", "CinemachineRotationComposer"), ["third_person"] = ("CinemachineThirdPersonFollow", "CinemachineRotationComposer"), diff --git a/MCPForUnity/Editor/Tools/Cameras/CameraHelpers.cs b/MCPForUnity/Editor/Tools/Cameras/CameraHelpers.cs index 80e75a3bb..d4f0b5e82 100644 --- a/MCPForUnity/Editor/Tools/Cameras/CameraHelpers.cs +++ b/MCPForUnity/Editor/Tools/Cameras/CameraHelpers.cs @@ -247,7 +247,8 @@ internal static string GetFallbackSuggestion(string action) { return action switch { - "set_body" or "set_aim" => "Use 'set_lens' and 'set_target' for basic camera configuration.", + "set_body" => "Use 'set_lens' and 'set_target' for basic camera configuration.", + "set_aim" => "Use 'set_lens' and 'set_target' for basic camera configuration.", "set_blend" => "Without Cinemachine, switch cameras by enabling/disabling Camera components.", "set_noise" => "Camera shake without Cinemachine requires a custom script.", "ensure_brain" => "CinemachineBrain requires the Cinemachine package. Basic Camera does not need a Brain.", @@ -260,7 +261,11 @@ internal static void MarkDirty(GameObject go) { if (go == null) return; EditorUtility.SetDirty(go); +#if UNITY_2021_2_OR_NEWER var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); +#else + var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); +#endif if (prefabStage != null) UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(prefabStage.scene); else diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs index 7588e2cfb..0a1d1c57c 100644 --- a/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -36,7 +36,7 @@ public HandlerInfo(string commandName, Func syncHandler, Func public static class CommandRegistry { - private static readonly Dictionary _handlers = new(); + private static readonly Dictionary _handlers = new Dictionary(); private static bool _initialized = false; /// @@ -363,7 +363,7 @@ private static Func> CreateAsyncHandlerDelegate(MethodInfo return null; } - if (rawResult is not Task task) + if (!(rawResult is Task task)) { throw new InvalidOperationException( $"Async handler '{commandName}' returned an object that is not a Task" diff --git a/MCPForUnity/Editor/Tools/ExecuteCode.cs b/MCPForUnity/Editor/Tools/ExecuteCode.cs index 34d3f6679..40303e990 100644 --- a/MCPForUnity/Editor/Tools/ExecuteCode.cs +++ b/MCPForUnity/Editor/Tools/ExecuteCode.cs @@ -122,7 +122,7 @@ private static object HandleExecute(JObject @params) private static object HandleGetHistory(JObject @params) { int limit = @params["limit"]?.Value() ?? 10; - limit = Math.Clamp(limit, 1, MaxHistoryEntries); + limit = Mathf.Clamp(limit, 1, MaxHistoryEntries); if (_history.Count == 0) return new SuccessResponse("No execution history.", new { total = 0, entries = new object[0] }); diff --git a/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs b/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs index 589374a15..641b858f5 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs @@ -72,7 +72,7 @@ public static List GetFuzzyPropertySuggestions(string userInput, List> PropertySuggestionCache = new(); + private static readonly Dictionary> PropertySuggestionCache = new Dictionary>(); /// /// Rule-based suggestions that mimic AI behavior for property matching. diff --git a/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs b/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs index 1ffa16bf7..d21bb1c38 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs @@ -250,7 +250,7 @@ internal static object SetComponentPropertiesInternal(GameObject targetGo, strin // Nested paths (e.g. "transform.position") need local handling // since ComponentOps doesn't support dot/bracket notation. - if (propName.Contains('.') || propName.Contains('[')) + if (propName.Contains(".") || propName.Contains("[")) { setResult = SetNestedProperty(targetComponent, propName, propValue, InputSerializer, out setError); } diff --git a/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs b/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs index 2bfce076c..3f5f4661a 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs @@ -5,6 +5,9 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; +#if !UNITY_2021_2_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEditorInternal; using UnityEngine; diff --git a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs index 7afa27510..eae9fef49 100644 --- a/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs +++ b/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs @@ -5,7 +5,11 @@ using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Tools; using Newtonsoft.Json.Linq; +#if UNITY_2021_2_OR_NEWER using UnityEditor.SceneManagement; +#else +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEngine; using UnityEngine.SceneManagement; using MCPForUnity.Runtime.Helpers; @@ -46,7 +50,7 @@ internal static List FindObjectsInternal( { if (targetToken?.Type == JTokenType.Integer) searchMethod = "by_id"; - else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/')) + else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains("/")) searchMethod = "by_path"; else searchMethod = "by_name"; diff --git a/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs b/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs index c410c32f0..d9ab52424 100644 --- a/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs +++ b/MCPForUnity/Editor/Tools/Graphics/GraphicsHelpers.cs @@ -257,7 +257,11 @@ internal static void MarkDirty(UnityEngine.Object obj) EditorUtility.SetDirty(obj); if (obj is Component comp) { - var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); + #if UNITY_2021_2_OR_NEWER + var prefabStage = UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); +#else + var prefabStage = UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage(); +#endif if (prefabStage != null) UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(prefabStage.scene); else diff --git a/MCPForUnity/Editor/Tools/Graphics/LightBakingOps.cs b/MCPForUnity/Editor/Tools/Graphics/LightBakingOps.cs index b2f904421..9232bdd33 100644 --- a/MCPForUnity/Editor/Tools/Graphics/LightBakingOps.cs +++ b/MCPForUnity/Editor/Tools/Graphics/LightBakingOps.cs @@ -151,7 +151,9 @@ internal static object GetSettings(JObject @params) ["indirectSampleCount"] = settings.indirectSampleCount, ["environmentSampleCount"] = settings.environmentSampleCount, ["mixedBakeMode"] = settings.mixedBakeMode.ToString(), +#if UNITY_2021_2_OR_NEWER ["lightmapCompression"] = settings.lightmapCompression.ToString(), +#endif ["ao"] = settings.ao, ["aoMaxDistance"] = settings.aoMaxDistance }; @@ -510,6 +512,7 @@ private static bool TrySetLightingSetting(LightingSettings settings, string name case "compress_lightmaps": case "lightmapcompression": case "lightmap_compression": +#if UNITY_2021_2_OR_NEWER var strVal = value?.ToString() ?? ""; if (System.Enum.TryParse(strVal, true, out var compression)) settings.lightmapCompression = compression; @@ -521,6 +524,9 @@ private static bool TrySetLightingSetting(LightingSettings settings, string name else return false; return true; +#else + return true; +#endif case "ao": settings.ao = ParamCoercion.CoerceBool(value, settings.ao); diff --git a/MCPForUnity/Editor/Tools/Graphics/RenderPipelineOps.cs b/MCPForUnity/Editor/Tools/Graphics/RenderPipelineOps.cs index 46b4f6ffe..be6a932ce 100644 --- a/MCPForUnity/Editor/Tools/Graphics/RenderPipelineOps.cs +++ b/MCPForUnity/Editor/Tools/Graphics/RenderPipelineOps.cs @@ -258,8 +258,15 @@ private static object ConvertPropertyValue(JToken value, Type targetType) if (targetType.IsEnum) { string str = value.ToString(); - if (Enum.TryParse(targetType, str, true, out object enumVal)) + try + { + object enumVal = Enum.Parse(targetType, str, true); return enumVal; + } + catch (ArgumentException) + { + // Not a defined enum value; fall through to int coercion. + } if (int.TryParse(str, out int intVal)) return Enum.ToObject(targetType, intVal); } diff --git a/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs b/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs index 5882505d4..a8b1614b3 100644 --- a/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs +++ b/MCPForUnity/Editor/Tools/Graphics/SkyboxOps.cs @@ -11,7 +11,7 @@ namespace MCPForUnity.Editor.Tools.Graphics { internal static class SkyboxOps { - static Texture CustomReflectionTexture + static Cubemap CustomReflectionTexture { get => #if UNITY_2022_1_OR_NEWER @@ -316,7 +316,7 @@ public static object SetReflection(JObject @params) string cubemapPath = p.Get("path") ?? p.Get("cubemap_path"); if (!string.IsNullOrEmpty(cubemapPath)) { - var cubemap = AssetDatabase.LoadAssetAtPath(cubemapPath); + var cubemap = AssetDatabase.LoadAssetAtPath(cubemapPath); if (cubemap != null) CustomReflectionTexture = cubemap; else @@ -417,8 +417,10 @@ private static object ReadMaterialProperty(Material mat, string propName, Shader case ShaderPropertyType.Float: case ShaderPropertyType.Range: return mat.GetFloat(propName); +#if UNITY_2021_2_OR_NEWER case ShaderPropertyType.Int: return mat.GetInt(propName); +#endif case ShaderPropertyType.Vector: var v = mat.GetVector(propName); return new[] { v.x, v.y, v.z, v.w }; @@ -453,9 +455,11 @@ private static bool SetMaterialProperty(Material mat, string propName, JToken va case ShaderPropertyType.Range: mat.SetFloat(propName, (float)value); return true; +#if UNITY_2021_2_OR_NEWER case ShaderPropertyType.Int: mat.SetInt(propName, (int)value); return true; +#endif case ShaderPropertyType.Vector: if (value is JArray vecArr && vecArr.Count >= 2) { diff --git a/MCPForUnity/Editor/Tools/Graphics/VolumeOps.cs b/MCPForUnity/Editor/Tools/Graphics/VolumeOps.cs index 69375f09a..ea5a8643b 100644 --- a/MCPForUnity/Editor/Tools/Graphics/VolumeOps.cs +++ b/MCPForUnity/Editor/Tools/Graphics/VolumeOps.cs @@ -629,8 +629,15 @@ private static object ConvertToParameterType(JToken value, Type targetType) if (targetType.IsEnum) { string str = value.ToString(); - if (Enum.TryParse(targetType, str, true, out object enumVal)) + try + { + object enumVal = Enum.Parse(targetType, str, true); return enumVal; + } + catch (ArgumentException) + { + // Not a defined enum value; fall through to int coercion. + } // Try as int if (int.TryParse(str, out int intVal)) return Enum.ToObject(targetType, intVal); diff --git a/MCPForUnity/Editor/Tools/ManageBuild.cs b/MCPForUnity/Editor/Tools/ManageBuild.cs index 8b5c89ac3..9594547fa 100644 --- a/MCPForUnity/Editor/Tools/ManageBuild.cs +++ b/MCPForUnity/Editor/Tools/ManageBuild.cs @@ -215,7 +215,11 @@ private static object HandlePlatform(ToolParams p) target = EditorUserBuildSettings.activeBuildTarget.ToString(), target_group = BuildTargetMapping.GetTargetGroup( EditorUserBuildSettings.activeBuildTarget).ToString(), +#if UNITY_2021_2_OR_NEWER subtarget = EditorUserBuildSettings.standaloneBuildSubtarget.ToString() +#else + subtarget = "player" +#endif }); } @@ -240,11 +244,13 @@ private static object HandlePlatform(ToolParams p) string subtargetStr = p.Get("subtarget"); if (!string.IsNullOrEmpty(subtargetStr)) { +#if UNITY_2021_2_OR_NEWER string subtargetLower = subtargetStr.ToLowerInvariant(); if (subtargetLower == "server") EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Server; else if (subtargetLower == "player") EditorUserBuildSettings.standaloneBuildSubtarget = StandaloneBuildSubtarget.Player; +#endif } // SwitchActiveBuildTarget is synchronous — blocks until reimport completes @@ -477,7 +483,11 @@ private static object HandleBatch(ToolParams p) if (EditorUserBuildSettings.activeBuildTarget != child.Target) EditorUserBuildSettings.SwitchActiveBuildTarget(group, child.Target); + #if UNITY_2021_2_OR_NEWER int subtarget = (int)StandaloneBuildSubtarget.Player; +#else + int subtarget = 0; +#endif var options = BuildRunner.CreateBuildOptions( child.Target, child.OutputPath, null, buildOpts, subtarget); BuildRunner.ScheduleBuild(child, options); diff --git a/MCPForUnity/Editor/Tools/ManageComponents.cs b/MCPForUnity/Editor/Tools/ManageComponents.cs index 0cb6757b9..616ab060d 100644 --- a/MCPForUnity/Editor/Tools/ManageComponents.cs +++ b/MCPForUnity/Editor/Tools/ManageComponents.cs @@ -6,6 +6,9 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; +#if !UNITY_2021_2_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEngine; using MCPForUnity.Runtime.Helpers; diff --git a/MCPForUnity/Editor/Tools/ManagePackages.cs b/MCPForUnity/Editor/Tools/ManagePackages.cs index a61398a55..0c70b6709 100644 --- a/MCPForUnity/Editor/Tools/ManagePackages.cs +++ b/MCPForUnity/Editor/Tools/ManagePackages.cs @@ -19,11 +19,11 @@ namespace MCPForUnity.Editor.Tools public static class ManagePackages { // Pending async requests keyed by job ID - private static readonly Dictionary PendingRequests = new(); + private static readonly Dictionary PendingRequests = new Dictionary(); // Pending list/search requests keyed by job ID - private static readonly Dictionary PendingListRequests = new(); - private static readonly Dictionary PendingSearchRequests = new(); + private static readonly Dictionary PendingListRequests = new Dictionary(); + private static readonly Dictionary PendingSearchRequests = new Dictionary(); public static object HandleCommand(JObject @params) { @@ -360,7 +360,7 @@ private static object GetPackageInfo(ToolParams p) try { - var allPackages = PackageInfo.GetAllRegisteredPackages(); + var allPackages = GetRegisteredPackages(); var info = allPackages.FirstOrDefault(pkg => string.Equals(pkg.name, package, StringComparison.OrdinalIgnoreCase)); @@ -593,7 +593,7 @@ private static object Ping() { try { - var allPackages = PackageInfo.GetAllRegisteredPackages(); + var allPackages = GetRegisteredPackages(); return new SuccessResponse( "Package manager is available.", new @@ -721,7 +721,7 @@ private static string[] GetDependentPackages(string packageName) { string name = PackageJobManager.ExtractPackageName(packageName); - var allPackages = PackageInfo.GetAllRegisteredPackages(); + var allPackages = GetRegisteredPackages(); return allPackages .Where(pkg => pkg.dependencies.Any(d => string.Equals(d.name, name, StringComparison.OrdinalIgnoreCase))) @@ -733,5 +733,26 @@ private static string[] GetDependentPackages(string packageName) return null; } } + + private static PackageInfo[] GetRegisteredPackages() + { +#if UNITY_2021_2_OR_NEWER + return PackageInfo.GetAllRegisteredPackages(); +#else + var request = UnityEditor.PackageManager.Client.List(true); + while (!request.IsCompleted) + { + System.Threading.Thread.Sleep(10); + } + if (request.IsCompleted && request.Status == UnityEditor.PackageManager.StatusCode.Success) + { + var list = new System.Collections.Generic.List(); + foreach (var pi in request.Result) list.Add(pi); + return list.ToArray(); + } + return new PackageInfo[0]; +#endif + } + } } diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs index c3c66b3fa..d84077b54 100644 --- a/MCPForUnity/Editor/Tools/ManageScene.cs +++ b/MCPForUnity/Editor/Tools/ManageScene.cs @@ -7,6 +7,9 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; +#if !UNITY_2021_2_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEngine; using UnityEngine.SceneManagement; diff --git a/MCPForUnity/Editor/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs index 4f94df606..9db728070 100644 --- a/MCPForUnity/Editor/Tools/ManageScript.cs +++ b/MCPForUnity/Editor/Tools/ManageScript.cs @@ -271,7 +271,7 @@ public static object HandleCommand(JObject @params) }).ToArray(); var result = new { diagnostics = diags }; - return ok ? new SuccessResponse("Validation completed.", result) + return ok ? (IMcpResponse)new SuccessResponse("Validation completed.", result) : new ErrorResponse("Validation failed.", result); } case "edit": diff --git a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs index 2ddd021c3..4e762dfa6 100644 --- a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs +++ b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs @@ -28,7 +28,7 @@ public static class ManageScriptableObject private const string CodeTargetNotFound = "target_not_found"; private const string CodeAssetCreateFailed = "asset_create_failed"; - private static readonly HashSet ValidActions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet ValidActions = new HashSet(StringComparer.OrdinalIgnoreCase) { // NOTE: Action strings are normalized by NormalizeAction() (lowercased, '_'/'-' removed), // so we only need the canonical normalized forms here. @@ -223,7 +223,7 @@ private static object HandleModify(JObject @params) return new ErrorResponse(CodeInvalidParams, new { message = "'patches' is required.", targetPath, targetGuid }); } - if (patchesToken is not JArray patches) + if (!(patchesToken is JArray patches)) { return new ErrorResponse(CodeInvalidParams, new { message = "'patches' must be an array.", targetPath, targetGuid }); } @@ -275,7 +275,7 @@ private static List ValidatePatches(UnityEngine.Object target, JArray pa for (int i = 0; i < patches.Count; i++) { - if (patches[i] is not JObject patchObj) + if (!(patches[i] is JObject patchObj)) { results.Add(new { index = i, propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." }); continue; @@ -450,7 +450,7 @@ private static (List results, List warnings) ApplyPatches(UnityE for (int i = 0; i < patches.Count; i++) { - if (patches[i] is not JObject patchObj) + if (!(patches[i] is JObject patchObj)) { results.Add(new { propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." }); continue; @@ -1137,7 +1137,7 @@ private static bool TrySetAnimationCurve(SerializedProperty prop, JToken valueTo var curve = new AnimationCurve(); foreach (var keyToken in keysArray) { - if (keyToken is not JObject keyObj) + if (!(keyToken is JObject keyObj)) { message = "Each keyframe must be an object with 'time' and 'value'."; return false; @@ -1299,7 +1299,7 @@ private static bool TryResolveTarget(JToken targetToken, out UnityEngine.Object targetGuid = null; error = null; - if (targetToken is not JObject targetObj) + if (!(targetToken is JObject targetObj)) { error = new ErrorResponse(CodeInvalidParams, new { message = "'target' must be an object with {guid|path}." }); return false; @@ -1424,7 +1424,7 @@ private static string SanitizeSlashes(string path) var s = AssetPathUtility.NormalizeSeparators(path); while (s.IndexOf("//", StringComparison.Ordinal) >= 0) { - s = s.Replace("//", "/", StringComparison.Ordinal); + s = s.Replace("//", "/"); } return s; } diff --git a/MCPForUnity/Editor/Tools/ManageUI.cs b/MCPForUnity/Editor/Tools/ManageUI.cs index cad174139..8baf302e8 100644 --- a/MCPForUnity/Editor/Tools/ManageUI.cs +++ b/MCPForUnity/Editor/Tools/ManageUI.cs @@ -16,7 +16,7 @@ namespace MCPForUnity.Editor.Tools [McpForUnityTool("manage_ui", AutoRegister = false, Group = "ui")] public static class ManageUI { - private static readonly HashSet ValidExtensions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet ValidExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".uxml", ".uss" }; @@ -296,6 +296,12 @@ private static object UpdateFile(JObject @params) new { path }); } +#if !UNITY_2021_2_OR_NEWER + private static object AttachUIDocument(JObject @params) + { + return new ErrorResponse("Attach UIDocument requires Unity 2021.2 or newer (UIDocument component)."); + } +#else private static object AttachUIDocument(JObject @params) { var p = new ToolParams(@params); @@ -391,7 +397,14 @@ private static object AttachUIDocument(JObject @params) sortOrder }); } +#endif +#if !UNITY_2021_2_OR_NEWER + private static object CreatePanelSettings(JObject @params) + { + return new ErrorResponse("PanelSettings requires Unity 2021.2 or newer."); + } +#else private static object CreatePanelSettings(JObject @params) { var p = new ToolParams(@params); @@ -458,7 +471,14 @@ private static object CreatePanelSettings(JObject @params) return new SuccessResponse($"Created PanelSettings at {path}", new { path, applied = changes }); } +#endif +#if !UNITY_2021_2_OR_NEWER + private static object UpdatePanelSettings(JObject @params) + { + return new ErrorResponse("PanelSettings requires Unity 2021.2 or newer."); + } +#else private static object UpdatePanelSettings(JObject @params) { var p = new ToolParams(@params); @@ -479,7 +499,7 @@ private static object UpdatePanelSettings(JObject @params) return new ErrorResponse($"No PanelSettings found at {path}"); JToken settingsToken = p.GetRaw("settings"); - if (settingsToken is not JObject settingsObj || settingsObj.Count == 0) + if (!(settingsToken is JObject settingsObj) || settingsObj.Count == 0) return new ErrorResponse("'settings' dict is required with at least one property to update."); var changes = new List(); @@ -494,7 +514,14 @@ private static object UpdatePanelSettings(JObject @params) return new SuccessResponse($"Updated PanelSettings at {path}", new { path, applied = changes }); } +#endif +#if !UNITY_2021_2_OR_NEWER + private static object CreateDefaultPanelSettings(string path) + { + return null; + } +#else private static PanelSettings CreateDefaultPanelSettings(string path) { string dir = Path.GetDirectoryName(path); @@ -508,6 +535,7 @@ private static PanelSettings CreateDefaultPanelSettings(string path) AssetDatabase.SaveAssets(); return ps; } +#endif /// /// Generic, data-driven applicator for PanelSettings properties. @@ -518,6 +546,11 @@ private static PanelSettings CreateDefaultPanelSettings(string path) /// clearColor, colorClearValue, clearDepthStencil, /// themeStyleSheet, dynamicAtlasSettings. /// +#if !UNITY_2021_2_OR_NEWER + private static void ApplyPanelSettingsProperties(object ps, JObject settings, List changes) + { + } +#else private static void ApplyPanelSettingsProperties(PanelSettings ps, JObject settings, List changes) { foreach (var prop in settings) @@ -603,7 +636,13 @@ private static void ApplyPanelSettingsProperties(PanelSettings ps, JObject setti } } } +#endif +#if !UNITY_2021_2_OR_NEWER + private static void ApplyDynamicAtlasSettings(object ps, JObject da, List changes) + { + } +#else private static void ApplyDynamicAtlasSettings(PanelSettings ps, JObject da, List changes) { var daCopy = ps.dynamicAtlasSettings; @@ -620,6 +659,7 @@ private static void ApplyDynamicAtlasSettings(PanelSettings ps, JObject da, List ps.dynamicAtlasSettings = daCopy; changes.Add("dynamicAtlasSettings"); } +#endif // ── Tiny helpers to keep the switch compact ───────────────────────── @@ -704,6 +744,12 @@ private static void EnsureFolderExists(string assetFolderPath) } } +#if !UNITY_2021_2_OR_NEWER + private static object GetVisualTree(JObject @params) + { + return new ErrorResponse("Get visual tree requires Unity 2021.2 or newer (UIDocument runtime UI)."); + } +#else private static object GetVisualTree(JObject @params) { var p = new ToolParams(@params); @@ -753,6 +799,7 @@ private static object GetVisualTree(JObject @params) tree }); } +#endif private static object SerializeVisualElement(VisualElement element, int depth, int maxDepth) { @@ -807,7 +854,7 @@ private static object SerializeVisualElement(VisualElement element, int depth, i // Persistent RenderTextures keyed by PanelSettings instance ID so the panel // renders into them automatically every frame. - private static readonly Dictionary s_panelRTs = new(); + private static readonly Dictionary s_panelRTs = new Dictionary(); // Play-mode coroutine capture state. Only one capture is in-flight at a // time; concurrent render_ui calls while a capture is pending are rejected @@ -816,6 +863,12 @@ private static object SerializeVisualElement(VisualElement element, int depth, i private static bool s_pendingCaptureDone; private static bool s_pendingCaptureStarted; +#if !UNITY_2021_2_OR_NEWER + private static object RenderUI(JObject @params) + { + return new ErrorResponse("Render UI requires Unity 2021.2 or newer (UIDocument runtime UI)."); + } +#else private static object RenderUI(JObject @params) { var p = new ToolParams(@params); @@ -1185,6 +1238,7 @@ private static object RenderUI(JObject @params) } } } +#endif // ---- Link Stylesheet ---- @@ -1386,6 +1440,12 @@ private static object ListUIAssets(JObject @params) // ---- Detach UIDocument ---- +#if !UNITY_2021_2_OR_NEWER + private static object DetachUIDocument(JObject @params) + { + return new ErrorResponse("Detach UIDocument requires Unity 2021.2 or newer (UIDocument component)."); + } +#else private static object DetachUIDocument(JObject @params) { var p = new ToolParams(@params); @@ -1421,9 +1481,16 @@ private static object DetachUIDocument(JObject @params) removedSourceAsset = sourceAsset, }); } +#endif // ---- Modify Visual Element ---- +#if !UNITY_2021_2_OR_NEWER + private static object ModifyVisualElement(JObject @params) + { + return new ErrorResponse("Modify visual element requires Unity 2021.2 or newer (UIDocument runtime UI)."); + } +#else private static object ModifyVisualElement(JObject @params) { var p = new ToolParams(@params); @@ -1580,6 +1647,7 @@ private static object ModifyVisualElement(JObject @params) $"Modified element '{elementName}' on {go.name}: {string.Join(", ", applied)}", responseData); } +#endif private static void ApplyInlineStyles(VisualElement element, JObject styleObj, List modifications) { diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs index 65d158a5d..11422c452 100644 --- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs +++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs @@ -6,6 +6,9 @@ using Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.SceneManagement; +#if !UNITY_2021_2_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif using UnityEngine; using UnityEngine.SceneManagement; using MCPForUnity.Runtime.Helpers; @@ -419,9 +422,24 @@ private static void ApplyPropertyBlockToMaterial(Renderer renderer, int slot, Ma string[] colorProps = { "_BaseColor", "_Color" }; foreach (string prop in colorProps) { - if (mat.HasProperty(prop) && block.HasColor(prop)) + if (mat.HasProperty(prop)) { - mat.SetColor(prop, block.GetColor(prop)); +#if UNITY_2021_2_OR_NEWER + if (block.HasColor(prop)) + { + mat.SetColor(prop, block.GetColor(prop)); + } +#else + try + { + Color c = block.GetColor(prop); + mat.SetColor(prop, c); + } + catch + { + // Property exists on material but not present in this block. + } +#endif } } } @@ -958,7 +976,7 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb continue; } - if (entry.Value is not JObject props || !props.HasValues) + if (!(entry.Value is JObject props) || !props.HasValues) { continue; } @@ -1311,10 +1329,18 @@ private static object OpenPrefabStage(string requestedPath) return new ErrorResponse($"Prefab asset not found at '{sanitizedPath}'."); } +#if UNITY_2021_2_OR_NEWER var prefabStage = PrefabStageUtility.OpenPrefab(sanitizedPath); bool enteredStage = prefabStage != null && string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase) && prefabStage.prefabContentsRoot != null; +#else + AssetDatabase.OpenAsset(prefabAsset); + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + bool enteredStage = prefabStage != null + && string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase) + && prefabStage.prefabContentsRoot != null; +#endif if (!enteredStage) { diff --git a/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs b/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs index c14482ebe..3a4cc5ebd 100644 --- a/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs +++ b/MCPForUnity/Editor/Tools/Profiler/Operations/CounterOps.cs @@ -144,8 +144,13 @@ void Tick() case "vr": return ProfilerCategory.Vr; case "internal": return ProfilerCategory.Internal; case "particles": return ProfilerCategory.Particles; +#if UNITY_2021_2_OR_NEWER case "fileio": return ProfilerCategory.FileIO; case "virtualtexturing": return ProfilerCategory.VirtualTexturing; +#else + case "fileio": return ProfilerCategory.Loading; + case "virtualtexturing": return ProfilerCategory.Render; +#endif default: error = $"Unknown category '{name}'. Valid: {string.Join(", ", ValidCategories)}"; return null; diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index 155484b6f..5d28b74f3 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -535,7 +535,7 @@ private static int FindStackStartIndex(string[] lines) ( trimmedLine.Length > 0 && char.IsUpper(trimmedLine[0]) - && trimmedLine.Contains('.') + && trimmedLine.Contains(".") ) ) { diff --git a/MCPForUnity/Editor/Tools/UnityReflect.cs b/MCPForUnity/Editor/Tools/UnityReflect.cs index 236b74342..8db7ce03e 100644 --- a/MCPForUnity/Editor/Tools/UnityReflect.cs +++ b/MCPForUnity/Editor/Tools/UnityReflect.cs @@ -17,8 +17,8 @@ namespace MCPForUnity.Editor.Tools public static class UnityReflect { private static Dictionary _assemblyTypeCache; - private static readonly object CacheLock = new(); - private static readonly ConcurrentDictionary ExtensionMethodCache = new(); + private static readonly object CacheLock = new object(); + private static readonly ConcurrentDictionary ExtensionMethodCache = new ConcurrentDictionary(); private static readonly string[] NamespacePrefixes = { @@ -40,7 +40,7 @@ public static class UnityReflect "UnityEngine.UIElements." }; - private static readonly Dictionary FriendlyTypeNames = new() + private static readonly Dictionary FriendlyTypeNames = new Dictionary() { { typeof(void), "void" }, { typeof(int), "int" }, @@ -146,7 +146,7 @@ private static object GetTypeInfo(ToolParams p) string normalizedName = NormalizeGenericName(className); // Check for ambiguity first (only for short names without namespace) - if (!normalizedName.Contains('.') && !normalizedName.Contains('`')) + if (!normalizedName.Contains(".") && !normalizedName.Contains("`")) { var matches = FindAllTypesByShortName(normalizedName); if (matches.Count > 1) @@ -265,7 +265,7 @@ private static object GetMemberInfo(ToolParams p) string memberName = memberResult.Value; string normalizedName = NormalizeGenericName(className); - if (!normalizedName.Contains('.') && !normalizedName.Contains('`')) + if (!normalizedName.Contains(".") && !normalizedName.Contains("`")) { var matches = FindAllTypesByShortName(normalizedName); if (matches.Count > 1) diff --git a/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs b/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs index 18e542b6e..5f3604575 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs @@ -189,8 +189,8 @@ public static object AddBurst(JObject @params) float time = @params["time"]?.ToObject() ?? 0f; int minCountRaw = @params["minCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30; int maxCountRaw = @params["maxCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30; - short minCount = (short)Math.Clamp(minCountRaw, 0, short.MaxValue); - short maxCount = (short)Math.Clamp(maxCountRaw, 0, short.MaxValue); + short minCount = (short)Mathf.Clamp(minCountRaw, 0, short.MaxValue); + short maxCount = (short)Mathf.Clamp(maxCountRaw, 0, short.MaxValue); int cycles = @params["cycles"]?.ToObject() ?? 1; float interval = @params["interval"]?.ToObject() ?? 0.01f; diff --git a/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs b/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs index 6b0e6d113..6a92b4d1b 100644 --- a/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs +++ b/MCPForUnity/Editor/Windows/Components/AssetGen/McpAssetGenSection.cs @@ -6,6 +6,7 @@ using MCPForUnity.Editor.Services.AssetGen.Import; using UnityEngine; using UnityEngine.UIElements; +using MCPForUnity.Editor.Windows.Components; namespace MCPForUnity.Editor.Windows.Components.AssetGen { @@ -39,7 +40,7 @@ private static readonly (string Id, string Label)[] ImageProviders = // UI Elements private VisualElement providersContainer; private VisualElement gltfastNotice; - private DropdownField formatDropdown; + private CompatDropdownField formatDropdown; private TextField outputRootField; private Toggle autoNormalizeToggle; private Button refreshButton; @@ -47,7 +48,7 @@ private static readonly (string Id, string Label)[] ImageProviders = // Per-provider enable toggles for the GLB-capable (model) providers, used to // recompute the glTFast notice when a toggle changes. - private readonly List<(string Id, Toggle Toggle)> modelEnableToggles = new(); + private readonly List<(string Id, Toggle Toggle)> modelEnableToggles = new List<(string Id, Toggle Toggle)>(); public VisualElement Root { get; private set; } @@ -63,7 +64,7 @@ private void CacheUIElements() { providersContainer = Root.Q("assetgen-providers-container"); gltfastNotice = Root.Q("gltfast-notice"); - formatDropdown = Root.Q("assetgen-format-dropdown"); + formatDropdown = Root.Q("assetgen-format-dropdown"); outputRootField = Root.Q("assetgen-output-root"); autoNormalizeToggle = Root.Q("assetgen-auto-normalize"); refreshButton = Root.Q