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..6a55825b6 --- /dev/null +++ b/MCPForUnity/Editor/Helpers/ProcessArgumentListCompat.cs @@ -0,0 +1,70 @@ +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 "\"\""; + } + + // Only quote when necessary; otherwise pass through verbatim so + // backslashes in paths (e.g. C:\Program Files\...) are preserved. + if (arg.IndexOfAny(new[] { ' ', '\t', '"' }) < 0) + { + return arg; + } + + // Standard Windows command-line quoting (the algorithm used by + // .NET's ArgumentList / CommandLineToArgvW): wrap in quotes; for every + // run of backslashes, double them only when immediately followed by a + // quote or at the very end of the argument (before the closing quote). + var sb = new System.Text.StringBuilder(); + sb.Append('"'); + int backslashes = 0; + foreach (char ch in arg) + { + if (ch == '\\') + { + backslashes++; + continue; + } + if (ch == '"') + { + sb.Append('\\', backslashes * 2 + 1); + sb.Append('"'); + backslashes = 0; + continue; + } + sb.Append('\\', backslashes); + backslashes = 0; + sb.Append(ch); + } + sb.Append('\\', backslashes * 2); + sb.Append('"'); + return sb.ToString(); + } + } +} 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..1cea906d9 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,6 +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(); +#if UNITY_2021_2_OR_NEWER using (var kdf = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256)) { byte[] material = kdf.GetBytes(64); @@ -100,8 +102,56 @@ private void DeriveKeys(out byte[] encKey, out byte[] macKey) Buffer.BlockCopy(material, 0, encKey, 0, 32); Buffer.BlockCopy(material, 32, macKey, 0, 32); } +#else + // Unity 2020.3 (netstandard2.0 profile) has no 4-arg Rfc2898DeriveBytes overload, so + // implement PBKDF2-HMAC-SHA256 (RFC 2898) manually to keep key derivation + // byte-identical with the 2021.2+ path (a 3-arg SHA1 derivation would make + // existing ciphertext fail MAC validation). + byte[] material = Pbkdf2Sha256(password, salt, Iterations, 64); + encKey = new byte[32]; + macKey = new byte[32]; + Buffer.BlockCopy(material, 0, encKey, 0, 32); + Buffer.BlockCopy(material, 32, macKey, 0, 32); +#endif } +#if !UNITY_2021_2_OR_NEWER + /// PBKDF2 with HMAC-SHA256 (RFC 2898), matching the .NET 4-arg Rfc2898DeriveBytes output. + private static byte[] Pbkdf2Sha256(string password, byte[] salt, int iterations, int numBytes) + { + var prf = new System.Security.Cryptography.HMACSHA256( + System.Text.Encoding.UTF8.GetBytes(password)); + int hLen = prf.HashSize / 8; + int blocks = (numBytes + hLen - 1) / hLen; + + var output = new byte[blocks * hLen]; + var saltPlusOne = new byte[salt.Length + 4]; + + for (int block = 1; block <= blocks; block++) + { + Buffer.BlockCopy(salt, 0, saltPlusOne, 0, salt.Length); + saltPlusOne[salt.Length] = (byte)((block >> 24) & 0xFF); + saltPlusOne[salt.Length + 1] = (byte)((block >> 16) & 0xFF); + saltPlusOne[salt.Length + 2] = (byte)((block >> 8) & 0xFF); + saltPlusOne[salt.Length + 3] = (byte)(block & 0xFF); + + byte[] u = prf.ComputeHash(saltPlusOne); + byte[] t = (byte[])u.Clone(); + for (int i = 1; i < iterations; i++) + { + u = prf.ComputeHash(u); + for (int j = 0; j < hLen; j++) t[j] ^= u[j]; + } + Buffer.BlockCopy(t, 0, output, (block - 1) * hLen, hLen); + } + + prf.Dispose(); + var result = new byte[numBytes]; + Buffer.BlockCopy(output, 0, result, 0, numBytes); + return result; + } +#endif + private byte[] Encrypt(byte[] plaintext) { DeriveKeys(out byte[] encKey, out byte[] macKey); @@ -210,8 +260,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..1e918bf0a 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 = RegisteredPackageInfo.GetRegisteredPackages(); var info = FindPackageInfo(allPackages, packageName, job.Package); if (job.Operation == "add" || job.Operation == "embed") @@ -179,7 +179,11 @@ 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 FindPackageInfo(PackageInfo[] allPackages, string packageName, string originalIdentifier) + + + + + private static RegisteredPackageInfo FindPackageInfo(RegisteredPackageInfo[] allPackages, string packageName, string originalIdentifier) { // Direct name match (handles normal com.company.package identifiers) var info = allPackages.FirstOrDefault(p => @@ -200,7 +204,7 @@ private static PackageInfo FindPackageInfo(PackageInfo[] allPackages, string pac return allPackages.FirstOrDefault(p => p.source == PackageSource.Git || p.source == PackageSource.Local ? p.packageId != null && p.packageId.Contains(originalIdentifier) - || p.resolvedPath != null && p.resolvedPath.Contains(originalIdentifier) + || p.name != null && p.name.Contains(originalIdentifier) : false); } 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/RegisteredPackageInfo.cs b/MCPForUnity/Editor/Services/RegisteredPackageInfo.cs new file mode 100644 index 000000000..cf6d04c13 --- /dev/null +++ b/MCPForUnity/Editor/Services/RegisteredPackageInfo.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor.PackageManager; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Cross-version snapshot of an installed package. + /// + /// Unity 2021.2+ has PackageInfo.GetAllRegisteredPackages() (synchronous). + /// Unity 2020.3 has no synchronous "list all packages" API: PackageManager.Client.List + /// only completes on the main thread, so blocking on it from a tool handler would + /// deadlock the editor. Instead we read the authoritative Packages/packages-lock.json + /// (pure synchronous file I/O, no editor pumping required) and map entries here. + /// + public sealed class RegisteredPackageInfo + { + public string name; + public string version; + public string displayName; + public PackageSource source; + public string packageId; + public string description; + public string resolvedPath; + public AuthorInfo author; + public List dependencies = new List(); + + public struct AuthorInfo + { + public string name; + } + + public struct DependencyInfo + { + public string name; + public string version; + } + + /// Read the project's resolved package list without editor pumping. + public static RegisteredPackageInfo[] GetRegisteredPackages() + { +#if UNITY_2021_2_OR_NEWER + return PackageInfo.GetAllRegisteredPackages() + .Select(p => new RegisteredPackageInfo + { + name = p.name, + version = p.version, + displayName = p.displayName, + source = p.source, + packageId = p.packageId, + description = p.description, + resolvedPath = p.resolvedPath, + author = p.author != null + ? new AuthorInfo { name = p.author.name } + : default, + dependencies = p.dependencies + .Select(d => new DependencyInfo { name = d.name, version = d.version }) + .ToList() + }) + .ToArray(); +#else + try + { + // Packages/packages-lock.json is maintained by Package Manager itself and + // contains every resolved package (direct + transitive) with version, source + // and direct dependencies. Reading it is synchronous and side-effect free. + string lockPath = Path.Combine(Directory.GetCurrentDirectory(), "Packages", "packages-lock.json"); + if (!File.Exists(lockPath)) + { + return new RegisteredPackageInfo[0]; + } + + JObject root = JObject.Parse(File.ReadAllText(lockPath)); + JObject deps = root["dependencies"] as JObject; + if (deps == null) + { + return new RegisteredPackageInfo[0]; + } + + var result = new List(); + foreach (var kv in deps) + { + JObject entry = kv.Value as JObject; + if (entry == null) continue; + + var info = new RegisteredPackageInfo { name = kv.Key }; + info.version = entry["version"]?.ToString() ?? string.Empty; + info.displayName = kv.Key; + PopulateMetadata(info); + + string source = entry["source"]?.ToString() ?? "registry"; + switch (source) + { + case "builtin": info.source = PackageSource.BuiltIn; break; + case "embedded": info.source = PackageSource.Embedded; break; + case "git": info.source = PackageSource.Git; break; + case "local": info.source = PackageSource.Local; break; + case "localTarball": info.source = PackageSource.LocalTarball; break; + case "tarball": info.source = PackageSource.LocalTarball; break; + default: info.source = PackageSource.Registry; break; + } + + // Best-effort packageId: registry/git entries carry a url; local/embedded + // entries use "file:" + path when resolvable, otherwise name@version. + string url = entry["url"]?.ToString(); + if (!string.IsNullOrEmpty(url)) + { + info.packageId = info.source == PackageSource.Git + ? url + "#" + info.version + : url; + } + else + { + info.packageId = string.IsNullOrEmpty(info.version) + ? info.name + : info.name + "@" + info.version; + } + + JObject depObj = entry["dependencies"] as JObject; + if (depObj != null) + { + foreach (var d in depObj) + { + info.dependencies.Add(new DependencyInfo + { + name = d.Key, + version = d.Value?.ToString() ?? string.Empty + }); + } + } + + result.Add(info); + } + return result.ToArray(); + } + catch + { + return new RegisteredPackageInfo[0]; + } +#endif + } + + /// + /// Best-effort metadata (description/author/resolvedPath) for 2020.3: + /// packages-lock.json does not carry them, so read the package's own + /// package.json when it exists on disk. Failures are non-fatal. + /// + private static void PopulateMetadata(RegisteredPackageInfo info) + { + try + { + string pkgJsonPath = null; + string localPath = Path.Combine(Directory.GetCurrentDirectory(), "Packages", info.name, "package.json"); + if (File.Exists(localPath)) + { + pkgJsonPath = localPath; + info.resolvedPath = Path.Combine(Directory.GetCurrentDirectory(), "Packages", info.name); + } + else + { + string cacheDir = Path.Combine(Directory.GetCurrentDirectory(), "Library", "PackageCache"); + if (Directory.Exists(cacheDir)) + { + string versioned = info.name + "@" + info.version; + string cachePath = Path.Combine(cacheDir, versioned, "package.json"); + if (File.Exists(cachePath)) + { + pkgJsonPath = cachePath; + info.resolvedPath = Path.Combine(cacheDir, versioned); + } + else + { + // PackageCache uses "-" suffixes for some sources; scan prefix match. + string prefix = info.name + "@"; + foreach (string dir in Directory.GetDirectories(cacheDir)) + { + if (dir.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + string cand = Path.Combine(dir, "package.json"); + if (File.Exists(cand)) + { + pkgJsonPath = cand; + info.resolvedPath = dir; + break; + } + } + } + } + } + } + + if (pkgJsonPath != null) + { + JObject pkg = JObject.Parse(File.ReadAllText(pkgJsonPath)); + info.description = pkg["description"]?.ToString() ?? string.Empty; + JObject authorObj = pkg["author"] as JObject; + if (authorObj != null) + { + info.author = new AuthorInfo { name = authorObj["name"]?.ToString() ?? string.Empty }; + } + else + { + string authorStr = pkg["author"]?.ToString(); + if (!string.IsNullOrEmpty(authorStr) && !authorStr.StartsWith("{")) + { + info.author = new AuthorInfo { name = authorStr }; + } + } + } + } + catch + { + // Non-fatal: metadata stays empty. + } + } + + } +} diff --git a/MCPForUnity/Editor/Services/RegisteredPackageInfo.cs.meta b/MCPForUnity/Editor/Services/RegisteredPackageInfo.cs.meta new file mode 100644 index 000000000..66c9caa8a --- /dev/null +++ b/MCPForUnity/Editor/Services/RegisteredPackageInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 37aa7fec72536814c81350a19073f9f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: 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 4c0ff0b87..fb814d15a 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; @@ -138,7 +138,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); } @@ -920,7 +920,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)); @@ -964,7 +964,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)); @@ -1133,7 +1133,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..1c8271e61 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,17 +23,19 @@ 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" }; + // GetArchitecture returns the BuildTargetGroup's architecture setting, + // where 0 means None (not x86_64); see PlayerSettings.SetArchitecture docs. + string archName = arch switch { 0 => "none", 1 => "arm64", 2 => "universal", _ => "unknown" }; return new { property, value = archName, raw = arch }; default: return null; } } - public static string WriteProperty(string property, string value, NamedBuildTarget namedTarget) + public static string WriteProperty(string property, string value, BuildTargetGroup namedTarget) { try { @@ -61,18 +63,19 @@ 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, + "none" => 0, + "default" => 0, "arm64" => 1, "universal" => 2, _ => -1 }; if (arch < 0) - return $"Unknown architecture '{value}'. Valid: x86_64, arm64, universal"; + return $"Unknown architecture '{value}'. Valid: none, arm64, universal"; PlayerSettings.SetArchitecture(namedTarget, arch); return null; default: 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 229c084d4..2f0b8b1ed 100644 --- a/MCPForUnity/Editor/Tools/ExecuteCode.cs +++ b/MCPForUnity/Editor/Tools/ExecuteCode.cs @@ -124,7 +124,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..81fb32010 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 }); } @@ -228,25 +232,35 @@ private static object HandlePlatform(ToolParams p) return new ErrorResponse( $"Platform '{target}' is not installed. Install it via Unity Hub."); - if (EditorUserBuildSettings.activeBuildTarget == target) - return new SuccessResponse("Already on this platform.", new - { - target = target.ToString() - }); - - // Capture previous target before switching - string previousTarget = EditorUserBuildSettings.activeBuildTarget.ToString(); - + // Process subtarget before the active-target short-circuit so a "server" + // request on Unity < 2021.2 errors out even when the platform is already active. 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; +#else + // Unity 2020.3 has no server subtarget; fail loudly instead of silently + // building the player variant. + string subtargetLower = subtargetStr.ToLowerInvariant(); + if (subtargetLower == "server") + return new ErrorResponse("subtarget 'server' requires Unity 2021.2 or newer (StandaloneBuildSubtarget API)."); +#endif } + if (EditorUserBuildSettings.activeBuildTarget == target) + return new SuccessResponse("Already on this platform.", new + { + target = target.ToString() + }); + + // Capture previous target before switching + string previousTarget = EditorUserBuildSettings.activeBuildTarget.ToString(); + // SwitchActiveBuildTarget is synchronous — blocks until reimport completes EditorUserBuildSettings.SwitchActiveBuildTarget(group, target); @@ -477,7 +491,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..a86589015 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 = RegisteredPackageInfo.GetRegisteredPackages(); var info = allPackages.FirstOrDefault(pkg => string.Equals(pkg.name, package, StringComparison.OrdinalIgnoreCase)); @@ -381,7 +381,7 @@ private static object GetPackageInfo(ToolParams p) description = info.description, source = info.source.ToString(), resolved_path = info.resolvedPath, - author = info.author?.name, + author = info.author.name, dependencies, dependency_count = dependencies.Length } @@ -593,7 +593,7 @@ private static object Ping() { try { - var allPackages = PackageInfo.GetAllRegisteredPackages(); + var allPackages = RegisteredPackageInfo.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 = RegisteredPackageInfo.GetRegisteredPackages(); return allPackages .Where(pkg => pkg.dependencies.Any(d => string.Equals(d.name, name, StringComparison.OrdinalIgnoreCase))) @@ -733,5 +733,8 @@ private static string[] GetDependentPackages(string packageName) return null; } } + + + } } 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