using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using MCPForUnity.Editor.Helpers; using MCPForUnity.Runtime.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; namespace MCPForUnity.Editor.Tools { [McpForUnityTool("unity_reflect", AutoRegister = false, Group = "docs")] public static class UnityReflect { private static Dictionary _assemblyTypeCache; private static readonly object CacheLock = new(); private static readonly ConcurrentDictionary ExtensionMethodCache = new(); private static readonly string[] NamespacePrefixes = { "UnityEngine.", "UnityEditor.", "UnityEngine.UI.", "Unity.Cinemachine.", "UnityEngine.AI.", "UnityEngine.Rendering.Universal.", "UnityEngine.Rendering.HighDefinition.", "UnityEngine.InputSystem.", "UnityEngine.ProBuilder.", "UnityEngine.Tilemaps.", "UnityEngine.EventSystems.", "UnityEngine.Rendering.", "UnityEngine.SceneManagement.", "UnityEngine.Animations.", "UnityEngine.Playables.", "UnityEngine.UIElements." }; private static readonly Dictionary FriendlyTypeNames = new() { { typeof(void), "void" }, { typeof(int), "int" }, { typeof(float), "float" }, { typeof(bool), "bool" }, { typeof(string), "string" }, { typeof(double), "double" }, { typeof(long), "long" }, { typeof(object), "object" }, { typeof(byte), "byte" }, { typeof(short), "short" }, { typeof(char), "char" }, { typeof(decimal), "decimal" }, { typeof(uint), "uint" }, { typeof(ulong), "ulong" }, { typeof(ushort), "ushort" }, { typeof(sbyte), "sbyte" } }; [InitializeOnLoadMethod] private static void OnLoad() { AssemblyReloadEvents.afterAssemblyReload += InvalidateCache; } private static void InvalidateCache() { lock (CacheLock) { _assemblyTypeCache = null; } ExtensionMethodCache.Clear(); } private static Dictionary GetAssemblyTypeCache() { lock (CacheLock) { if (_assemblyTypeCache != null) return _assemblyTypeCache; _assemblyTypeCache = new Dictionary(); foreach (var asm in UnityAssembliesCompat.GetLoadedAssemblies()) { try { _assemblyTypeCache[asm.FullName] = asm.GetExportedTypes(); } catch { // Some assemblies throw on GetExportedTypes } } return _assemblyTypeCache; } } public static object HandleCommand(JObject @params) { if (EditorApplication.isCompiling) return new ErrorResponse("Cannot reflect while Unity is compiling. Wait for domain reload to complete."); if (@params == null) return new ErrorResponse("Parameters cannot be null."); var p = new ToolParams(@params); var actionResult = p.GetRequired("action"); if (!actionResult.IsSuccess) return new ErrorResponse(actionResult.ErrorMessage); string action = actionResult.Value.ToLowerInvariant(); try { switch (action) { case "get_type": return GetTypeInfo(p); case "get_member": return GetMemberInfo(p); case "search": return SearchTypes(p); default: return new ErrorResponse( $"Unknown action: '{action}'. Supported actions: get_type, get_member, search."); } } catch (Exception ex) { return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace }); } } // === get_type === private static object GetTypeInfo(ToolParams p) { var classResult = p.GetRequired("class_name", "'class_name' parameter is required for get_type."); if (!classResult.IsSuccess) return new ErrorResponse(classResult.ErrorMessage); string className = classResult.Value; string normalizedName = NormalizeGenericName(className); // Check for ambiguity first (only for short names without namespace) if (!normalizedName.Contains('.') && !normalizedName.Contains('`')) { var matches = FindAllTypesByShortName(normalizedName); if (matches.Count > 1) { return new SuccessResponse($"Ambiguous type name '{className}'.", new { found = true, ambiguous = true, query = className, matches = matches.Select(t => t.FullName).OrderBy(n => n).ToArray(), hint = "Use the fully qualified name (e.g., 'UnityEngine.UI.Button') to disambiguate." }); } } var type = ResolveType(normalizedName); if (type == null) { return new SuccessResponse($"Type '{className}' not found.", new { found = false, query = className }); } // Open generic type definitions (e.g. List) segfault Mono on Unity 2021.3 // in mono_metadata_generic_param_equal_internal when any member reflection is // performed. Return minimal info using only safe property accesses. if (type.IsGenericTypeDefinition) { return new SuccessResponse($"Type info for '{type.Name}'.", new { found = true, name = type.Name, full_name = type.FullName, @namespace = type.Namespace, assembly = type.Assembly.GetName().Name, is_generic_type_definition = true, hint = "Open generic type — consult docs for member details." }); } var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; var methods = type.GetMethods(flags) .Where(m => !m.IsSpecialName) .Select(m => m.Name) .Distinct() .OrderBy(n => n) .ToArray(); var properties = type.GetProperties(flags) .Select(pr => pr.Name) .Distinct() .OrderBy(n => n) .ToArray(); var fields = type.GetFields(flags) .Select(f => f.Name) .Distinct() .OrderBy(n => n) .ToArray(); var events = type.GetEvents(flags) .Select(e => e.Name) .Distinct() .OrderBy(n => n) .ToArray(); var obsoleteMembers = GetObsoleteMembers(type, flags); var extensionMethods = FindExtensionMethods(type); var interfaces = type.GetInterfaces() .Select(i => FormatTypeName(i)) .OrderBy(n => n) .ToArray(); return new SuccessResponse($"Type info for '{FormatTypeName(type)}'.", new { found = true, name = FormatTypeName(type), full_name = type.FullName, @namespace = type.Namespace, assembly = type.Assembly.GetName().Name, base_class = type.BaseType != null ? FormatTypeName(type.BaseType) : null, interfaces, is_abstract = type.IsAbstract, is_sealed = type.IsSealed, is_static = type.IsAbstract && type.IsSealed, is_enum = type.IsEnum, is_interface = type.IsInterface, members = new { methods, properties, fields, events }, extension_methods = extensionMethods, obsolete_members = obsoleteMembers }); } // === get_member === private static object GetMemberInfo(ToolParams p) { var classResult = p.GetRequired("class_name", "'class_name' parameter is required for get_member."); if (!classResult.IsSuccess) return new ErrorResponse(classResult.ErrorMessage); var memberResult = p.GetRequired("member_name", "'member_name' parameter is required for get_member."); if (!memberResult.IsSuccess) return new ErrorResponse(memberResult.ErrorMessage); string className = classResult.Value; string memberName = memberResult.Value; string normalizedName = NormalizeGenericName(className); if (!normalizedName.Contains('.') && !normalizedName.Contains('`')) { var matches = FindAllTypesByShortName(normalizedName); if (matches.Count > 1) { return new SuccessResponse($"Ambiguous type name '{className}'.", new { found = true, ambiguous = true, query = className, matches = matches.Select(t => t.FullName).OrderBy(n => n).ToArray(), hint = "Use the fully qualified name to disambiguate before requesting member details." }); } } var type = ResolveType(normalizedName); if (type == null) { return new SuccessResponse($"Type '{className}' not found.", new { found = false, query = className }); } if (type.IsGenericTypeDefinition) { return new SuccessResponse( $"Open generic type '{type.Name}' — consult docs for member details.", new { found = false, type_name = type.FullName, member_name = memberName, is_generic_type_definition = true, hint = "Open generic type — consult docs for member details." }); } // Use flags without DeclaredOnly to find inherited members var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; // Try methods first var methods = type.GetMethods(flags) .Where(m => !m.IsSpecialName && m.Name == memberName) .ToArray(); if (methods.Length > 0) { var overloads = methods.Select(m => FormatMethodDetail(m)).ToArray(); return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new { found = true, type_name = FormatTypeName(type), member_name = memberName, member_type = "method", overload_count = overloads.Length, overloads }); } // Try properties var prop = type.GetProperty(memberName, flags); if (prop != null) { return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new { found = true, type_name = FormatTypeName(type), member_name = memberName, member_type = "property", property_type = FormatTypeName(prop.PropertyType), can_read = prop.CanRead, can_write = prop.CanWrite, is_static = (prop.GetMethod ?? prop.SetMethod)?.IsStatic ?? false, is_obsolete = prop.GetCustomAttribute() != null, declaring_type = prop.DeclaringType != type ? FormatTypeName(prop.DeclaringType) : null }); } // Try fields var field = type.GetField(memberName, flags); if (field != null) { return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new { found = true, type_name = FormatTypeName(type), member_name = memberName, member_type = "field", field_type = FormatTypeName(field.FieldType), is_static = field.IsStatic, is_readonly = field.IsInitOnly, is_constant = field.IsLiteral, constant_value = field.IsLiteral ? field.GetRawConstantValue() : null, is_obsolete = field.GetCustomAttribute() != null, declaring_type = field.DeclaringType != type ? FormatTypeName(field.DeclaringType) : null }); } // Try events var evt = type.GetEvent(memberName, flags); if (evt != null) { return new SuccessResponse($"Member '{memberName}' on '{FormatTypeName(type)}'.", new { found = true, type_name = FormatTypeName(type), member_name = memberName, member_type = "event", event_handler_type = FormatTypeName(evt.EventHandlerType), is_obsolete = evt.GetCustomAttribute() != null, declaring_type = evt.DeclaringType != type ? FormatTypeName(evt.DeclaringType) : null }); } // Try extension methods as a last resort var extMethods = FindExtensionMethodInfos(type, memberName); if (extMethods.Length > 0) { var overloads = extMethods.Select(m => FormatMethodDetail(m)).ToArray(); return new SuccessResponse($"Extension method '{memberName}' on '{FormatTypeName(type)}'.", new { found = true, type_name = FormatTypeName(type), member_name = memberName, member_type = "extension_method", overload_count = overloads.Length, overloads, declaring_type = FormatTypeName(extMethods[0].DeclaringType) }); } return new SuccessResponse($"Member '{memberName}' not found on '{FormatTypeName(type)}'.", new { found = false, type_name = FormatTypeName(type), member_name = memberName }); } // === search === private static object SearchTypes(ToolParams p) { var queryResult = p.GetRequired("query", "'query' parameter is required for search."); if (!queryResult.IsSuccess) return new ErrorResponse(queryResult.ErrorMessage); string query = queryResult.Value; string scope = p.Get("scope", "unity").ToLowerInvariant(); if (scope != "unity" && scope != "packages" && scope != "project" && scope != "all") { return new ErrorResponse( $"Invalid scope: '{scope}'. Supported: unity, packages, project, all."); } var cache = GetAssemblyTypeCache(); string queryLower = query.ToLowerInvariant(); var candidates = new List<(Type type, int rank)>(); foreach (var kvp in cache) { var asm = kvp.Value.Length > 0 ? kvp.Value[0].Assembly : null; if (asm == null) continue; string asmName = asm.GetName().Name; if (!MatchesScope(asmName, scope)) continue; foreach (var t in kvp.Value) { if (t.Name == null) continue; string nameLower = t.Name.ToLowerInvariant(); string fullNameLower = t.FullName?.ToLowerInvariant() ?? nameLower; if (nameLower == queryLower || fullNameLower == queryLower) candidates.Add((t, 0)); // Exact match else if (nameLower.StartsWith(queryLower)) candidates.Add((t, 1)); // Starts with else if (nameLower.Contains(queryLower) || fullNameLower.Contains(queryLower)) candidates.Add((t, 2)); // Contains } } var results = candidates .OrderBy(c => c.rank) .ThenBy(c => c.type.FullName) .Take(25) .Select(c => new { name = c.type.Name, full_name = c.type.FullName, @namespace = c.type.Namespace, assembly = c.type.Assembly.GetName().Name, is_class = c.type.IsClass, is_enum = c.type.IsEnum, is_interface = c.type.IsInterface, is_struct = c.type.IsValueType && !c.type.IsEnum }) .ToArray(); return new SuccessResponse($"Found {results.Length} type(s) matching '{query}' (scope: {scope}).", new { query, scope, count = results.Length, results, truncated = candidates.Count > 25 }); } // --- Type Resolution --- private static Type ResolveType(string className) { // Use the shared UnityTypeResolver which handles caching, // namespace prefixes, player-over-editor priority, and TypeCache fallback. var type = UnityTypeResolver.ResolveAny(className); if (type != null) return type; // UnityTypeResolver doesn't try our extended namespace prefixes, // so fall back to assembly cache scan for edge cases. var cache = GetAssemblyTypeCache(); foreach (var prefix in NamespacePrefixes) { string fullName = prefix + className; foreach (var kvp in cache) { type = Array.Find(kvp.Value, t => t.FullName == fullName); if (type != null) return type; } } return null; } private static List FindAllTypesByShortName(string shortName) { var matches = new List(); foreach (var kvp in GetAssemblyTypeCache()) { foreach (var t in kvp.Value) { if (t.Name == shortName) matches.Add(t); } } return matches; } // --- Generic Name Normalization --- private static string NormalizeGenericName(string name) { // Parse List -> List`1, Dictionary -> Dictionary`2 var match = Regex.Match(name, @"^(.+)<(.+)>$"); if (!match.Success) return name; string baseName = match.Groups[1].Value; string typeArgs = match.Groups[2].Value; // Count generic args by tracking nesting depth int argCount = 1; int depth = 0; foreach (char c in typeArgs) { if (c == '<') depth++; else if (c == '>') depth--; else if (c == ',' && depth == 0) argCount++; } return $"{baseName}`{argCount}"; } // --- Type Name Formatting --- private static string FormatTypeName(Type type) { if (type == null) return "null"; if (FriendlyTypeNames.TryGetValue(type, out var friendly)) return friendly; if (type.IsArray) return FormatTypeName(type.GetElementType()) + "[]"; if (type.IsByRef) return FormatTypeName(type.GetElementType()); if (type.IsGenericType) { string baseName = type.Name; int backtickIndex = baseName.IndexOf('`'); if (backtickIndex > 0) baseName = baseName.Substring(0, backtickIndex); var args = type.GetGenericArguments(); string argsStr = string.Join(", ", args.Select(FormatTypeName)); return $"{baseName}<{argsStr}>"; } // For nested types, use dot notation if (type.IsNested && type.DeclaringType != null) return FormatTypeName(type.DeclaringType) + "." + type.Name; return type.Name; } // --- Method Formatting --- private static object FormatMethodDetail(MethodInfo m) { var parameters = m.GetParameters().Select(param => { string prefix = ""; if (param.IsOut) prefix = "out "; else if (param.ParameterType.IsByRef) prefix = "ref "; return new { name = param.Name, type = prefix + FormatTypeName(param.ParameterType), has_default = param.HasDefaultValue, default_value = param.HasDefaultValue ? FormatDefaultValue(param.DefaultValue) : null, is_params = param.IsDefined(typeof(ParamArrayAttribute)) }; }).ToArray(); string signature = FormatMethodSignature(m); var obsoleteAttr = m.GetCustomAttribute(); return new { signature, return_type = FormatTypeName(m.ReturnType), parameters, is_static = m.IsStatic, is_virtual = m.IsVirtual && !m.IsFinal, is_abstract = m.IsAbstract, is_generic = m.IsGenericMethod, generic_arguments = m.IsGenericMethod ? m.GetGenericArguments().Select(a => a.Name).ToArray() : null, is_obsolete = obsoleteAttr != null, obsolete_message = obsoleteAttr?.Message, declaring_type = m.DeclaringType != m.ReflectedType ? FormatTypeName(m.DeclaringType) : null }; } private static string FormatMethodSignature(MethodInfo m) { string staticPrefix = m.IsStatic ? "static " : ""; string returnType = FormatTypeName(m.ReturnType); string name = m.Name; if (m.IsGenericMethod) { var genArgs = m.GetGenericArguments(); name += "<" + string.Join(", ", genArgs.Select(a => a.Name)) + ">"; } var paramStrings = m.GetParameters().Select(param => { string prefix = ""; if (param.IsOut) prefix = "out "; else if (param.ParameterType.IsByRef) prefix = "ref "; if (param.IsDefined(typeof(ParamArrayAttribute))) prefix = "params "; return prefix + FormatTypeName(param.ParameterType) + " " + param.Name; }); return $"{staticPrefix}{returnType} {name}({string.Join(", ", paramStrings)})"; } private static object FormatDefaultValue(object value) { if (value == null) return "null"; if (value is string s) return $"\"{s}\""; if (value is bool b) return b ? "true" : "false"; return value; } // --- Obsolete Member Detection --- private static string[] GetObsoleteMembers(Type type, BindingFlags flags) { var obsolete = new HashSet(); foreach (var m in type.GetMethods(flags).Where(m => !m.IsSpecialName)) { if (m.GetCustomAttribute() != null) obsolete.Add(m.Name); } foreach (var pr in type.GetProperties(flags)) { if (pr.GetCustomAttribute() != null) obsolete.Add(pr.Name); } foreach (var f in type.GetFields(flags)) { if (f.GetCustomAttribute() != null) obsolete.Add(f.Name); } foreach (var e in type.GetEvents(flags)) { if (e.GetCustomAttribute() != null) obsolete.Add(e.Name); } return obsolete.Count > 0 ? obsolete.OrderBy(n => n).ToArray() : Array.Empty(); } // --- Extension Method Discovery --- private static string[] FindExtensionMethods(Type targetType) { if (ExtensionMethodCache.TryGetValue(targetType, out var cached)) return cached; var extensionNames = new HashSet(); var cache = GetAssemblyTypeCache(); foreach (var kvp in cache) { // Extract assembly name from the FullName key (e.g., "UnityEngine, Version=...") string asmName = kvp.Key.Split(',')[0]; if (!asmName.StartsWith("UnityEngine") && !asmName.StartsWith("UnityEditor") && !asmName.StartsWith("Unity.")) continue; foreach (var t in kvp.Value) { if (!t.IsAbstract || !t.IsSealed) continue; // Static classes only if (!t.IsDefined(typeof(ExtensionAttribute), false)) continue; foreach (var method in t.GetMethods(BindingFlags.Public | BindingFlags.Static)) { if (!method.IsDefined(typeof(ExtensionAttribute), false)) continue; var firstParam = method.GetParameters().FirstOrDefault(); if (firstParam == null) continue; var paramType = firstParam.ParameterType; if (paramType.IsAssignableFrom(targetType) || targetType.IsSubclassOf(paramType) || paramType == targetType || (paramType.IsGenericType && IsGenericMatch(paramType, targetType))) { extensionNames.Add(method.Name); } } } } var result = extensionNames.Count > 0 ? extensionNames.OrderBy(n => n).ToArray() : Array.Empty(); ExtensionMethodCache.TryAdd(targetType, result); return result; } private static MethodInfo[] FindExtensionMethodInfos(Type targetType, string methodName) { var results = new List(); var cache = GetAssemblyTypeCache(); foreach (var kvp in cache) { string asmName = kvp.Key.Split(',')[0]; if (!asmName.StartsWith("UnityEngine") && !asmName.StartsWith("UnityEditor") && !asmName.StartsWith("Unity.")) continue; foreach (var t in kvp.Value) { if (!t.IsAbstract || !t.IsSealed) continue; if (!t.IsDefined(typeof(ExtensionAttribute), false)) continue; foreach (var method in t.GetMethods(BindingFlags.Public | BindingFlags.Static)) { if (method.Name != methodName) continue; if (!method.IsDefined(typeof(ExtensionAttribute), false)) continue; var firstParam = method.GetParameters().FirstOrDefault(); if (firstParam == null) continue; var paramType = firstParam.ParameterType; if (paramType.IsAssignableFrom(targetType) || targetType.IsSubclassOf(paramType) || paramType == targetType || (paramType.IsGenericType && IsGenericMatch(paramType, targetType))) { results.Add(method); } } } } return results.ToArray(); } private static bool IsGenericMatch(Type genericParamType, Type targetType) { if (!genericParamType.IsGenericType) return false; var genDef = genericParamType.GetGenericTypeDefinition(); // Check if targetType implements or inherits from the generic definition if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == genDef) return true; foreach (var iface in targetType.GetInterfaces()) { if (iface.IsGenericType && iface.GetGenericTypeDefinition() == genDef) return true; } return false; } // --- Scope Matching --- private static bool MatchesScope(string assemblyName, string scope) { switch (scope) { case "unity": return assemblyName.StartsWith("UnityEngine") || assemblyName.StartsWith("UnityEditor") || assemblyName.StartsWith("Unity."); case "packages": return !assemblyName.StartsWith("System") && !assemblyName.StartsWith("mscorlib") && !assemblyName.StartsWith("netstandard"); case "project": return assemblyName == "Assembly-CSharp" || assemblyName == "Assembly-CSharp-Editor" || assemblyName.StartsWith("Assembly-CSharp-firstpass") || assemblyName.StartsWith("Assembly-CSharp-Editor-firstpass"); case "all": return true; default: return false; } } } }