|
| 1 | +#nullable enable |
1 | 2 | using System; |
2 | 3 | using System.Collections.Generic; |
3 | 4 | using System.Linq; |
4 | 5 | using System.Reflection; |
5 | 6 | using Newtonsoft.Json; // Added for JsonSerializationException |
6 | 7 | using Newtonsoft.Json.Linq; |
7 | 8 | using UnityEditor; |
| 9 | +using UnityEditor.Compilation; // For CompilationPipeline |
8 | 10 | using UnityEditor.SceneManagement; |
9 | 11 | using UnityEditorInternal; |
10 | 12 | using UnityEngine; |
@@ -1097,6 +1099,29 @@ string searchMethod |
1097 | 1099 |
|
1098 | 1100 | // --- Internal Helpers --- |
1099 | 1101 |
|
| 1102 | + /// <summary> |
| 1103 | + /// Parses a JArray like [x, y, z] into a Vector3. |
| 1104 | + /// </summary> |
| 1105 | + private static Vector3? ParseVector3(JArray array) |
| 1106 | + { |
| 1107 | + if (array != null && array.Count == 3) |
| 1108 | + { |
| 1109 | + try |
| 1110 | + { |
| 1111 | + return new Vector3( |
| 1112 | + array[0].ToObject<float>(), |
| 1113 | + array[1].ToObject<float>(), |
| 1114 | + array[2].ToObject<float>() |
| 1115 | + ); |
| 1116 | + } |
| 1117 | + catch (Exception ex) |
| 1118 | + { |
| 1119 | + Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}"); |
| 1120 | + } |
| 1121 | + } |
| 1122 | + return null; |
| 1123 | + } |
| 1124 | + |
1100 | 1125 | /// <summary> |
1101 | 1126 | /// Finds a single GameObject based on token (ID, name, path) and search method. |
1102 | 1127 | /// </summary> |
@@ -2070,58 +2095,137 @@ public static UnityEngine.Object FindObjectByInstruction(JObject instruction, Ty |
2070 | 2095 |
|
2071 | 2096 |
|
2072 | 2097 | /// <summary> |
2073 | | - /// Helper to find a Type by name, searching relevant assemblies. |
| 2098 | + /// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs. |
| 2099 | + /// Searches already-loaded assemblies, prioritizing runtime script assemblies. |
2074 | 2100 | /// </summary> |
2075 | 2101 | private static Type FindType(string typeName) |
2076 | 2102 | { |
2077 | | - if (string.IsNullOrEmpty(typeName)) |
2078 | | - return null; |
| 2103 | + if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error)) |
| 2104 | + { |
| 2105 | + return resolvedType; |
| 2106 | + } |
| 2107 | + |
| 2108 | + // Log the resolver error if type wasn't found |
| 2109 | + if (!string.IsNullOrEmpty(error)) |
| 2110 | + { |
| 2111 | + Debug.LogWarning($"[FindType] {error}"); |
| 2112 | + } |
| 2113 | + |
| 2114 | + return null; |
| 2115 | + } |
| 2116 | + } |
| 2117 | + |
| 2118 | + /// <summary> |
| 2119 | + /// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions. |
| 2120 | + /// Prioritizes runtime (Player) assemblies over Editor assemblies. |
| 2121 | + /// </summary> |
| 2122 | + internal static class ComponentResolver |
| 2123 | + { |
| 2124 | + private static readonly Dictionary<string, Type> CacheByFqn = new(StringComparer.Ordinal); |
| 2125 | + private static readonly Dictionary<string, Type> CacheByName = new(StringComparer.Ordinal); |
2079 | 2126 |
|
2080 | | - // Handle fully qualified names first |
2081 | | - Type type = Type.GetType(typeName); |
2082 | | - if (type != null) return type; |
| 2127 | + /// <summary> |
| 2128 | + /// Resolve a Component/MonoBehaviour type by short or fully-qualified name. |
| 2129 | + /// Prefers runtime (Player) script assemblies; falls back to Editor assemblies. |
| 2130 | + /// Never uses Assembly.LoadFrom. |
| 2131 | + /// </summary> |
| 2132 | + public static bool TryResolve(string nameOrFullName, out Type type, out string error) |
| 2133 | + { |
| 2134 | + error = string.Empty; |
| 2135 | + |
| 2136 | + // 1) Exact FQN via Type.GetType |
| 2137 | + if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true; |
| 2138 | + type = Type.GetType(nameOrFullName, throwOnError: false); |
| 2139 | + if (IsValidComponent(type)) { Cache(type); return true; } |
| 2140 | + |
| 2141 | + // 2) Search loaded assemblies (prefer Player assemblies) |
| 2142 | + var candidates = FindCandidates(nameOrFullName); |
| 2143 | + if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } |
| 2144 | + if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } |
| 2145 | + |
| 2146 | +#if UNITY_EDITOR |
| 2147 | + // 3) Last resort: Editor-only TypeCache (fast index) |
| 2148 | + var tc = TypeCache.GetTypesDerivedFrom<Component>() |
| 2149 | + .Where(t => NamesMatch(t, nameOrFullName)); |
| 2150 | + candidates = PreferPlayer(tc).ToList(); |
| 2151 | + if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; } |
| 2152 | + if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; } |
| 2153 | +#endif |
| 2154 | + |
| 2155 | + error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " + |
| 2156 | + "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled."; |
| 2157 | + type = null!; |
| 2158 | + return false; |
| 2159 | + } |
2083 | 2160 |
|
2084 | | - // Handle common namespaces implicitly (add more as needed) |
2085 | | - string[] namespaces = { "UnityEngine", "UnityEngine.UI", "UnityEngine.AI", "UnityEngine.Animations", "UnityEngine.Audio", "UnityEngine.EventSystems", "UnityEngine.InputSystem", "UnityEngine.Networking", "UnityEngine.Rendering", "UnityEngine.SceneManagement", "UnityEngine.Tilemaps", "UnityEngine.U2D", "UnityEngine.Video", "UnityEditor", "UnityEditor.AI", "UnityEditor.Animations", "UnityEditor.Experimental.GraphView", "UnityEditor.IMGUI.Controls", "UnityEditor.PackageManager.UI", "UnityEditor.SceneManagement", "UnityEditor.UI", "UnityEditor.U2D", "UnityEditor.VersionControl" }; // Add more relevant namespaces |
| 2161 | + private static bool NamesMatch(Type t, string q) => |
| 2162 | + t.Name.Equals(q, StringComparison.Ordinal) || |
| 2163 | + (t.FullName?.Equals(q, StringComparison.Ordinal) ?? false); |
2086 | 2164 |
|
2087 | | - foreach (string ns in namespaces) { |
2088 | | - type = Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}.CoreModule") ?? // Heuristic: Check CoreModule first for UnityEngine/UnityEditor |
2089 | | - Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}"); // Try assembly matching namespace root |
2090 | | - if (type != null) return type; |
2091 | | - } |
| 2165 | + private static bool IsValidComponent(Type? t) => |
| 2166 | + t != null && typeof(Component).IsAssignableFrom(t); |
| 2167 | + |
| 2168 | + private static void Cache(Type t) |
| 2169 | + { |
| 2170 | + if (t.FullName != null) CacheByFqn[t.FullName] = t; |
| 2171 | + CacheByName[t.Name] = t; |
| 2172 | + } |
2092 | 2173 |
|
| 2174 | + private static List<Type> FindCandidates(string query) |
| 2175 | + { |
| 2176 | + bool isShort = !query.Contains('.'); |
| 2177 | + var loaded = AppDomain.CurrentDomain.GetAssemblies(); |
| 2178 | + |
| 2179 | +#if UNITY_EDITOR |
| 2180 | + // Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp) |
| 2181 | + var playerAsmNames = new HashSet<string>( |
| 2182 | + UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), |
| 2183 | + StringComparer.Ordinal); |
| 2184 | + |
| 2185 | + IEnumerable<System.Reflection.Assembly> playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name)); |
| 2186 | + IEnumerable<System.Reflection.Assembly> editorAsms = loaded.Except(playerAsms); |
| 2187 | +#else |
| 2188 | + IEnumerable<System.Reflection.Assembly> playerAsms = loaded; |
| 2189 | + IEnumerable<System.Reflection.Assembly> editorAsms = Array.Empty<System.Reflection.Assembly>(); |
| 2190 | +#endif |
| 2191 | + static IEnumerable<Type> SafeGetTypes(System.Reflection.Assembly a) |
| 2192 | + { |
| 2193 | + try { return a.GetTypes(); } |
| 2194 | + catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; } |
| 2195 | + } |
| 2196 | + |
| 2197 | + Func<Type, bool> match = isShort |
| 2198 | + ? (t => t.Name.Equals(query, StringComparison.Ordinal)) |
| 2199 | + : (t => t.FullName!.Equals(query, StringComparison.Ordinal)); |
| 2200 | + |
| 2201 | + var fromPlayer = playerAsms.SelectMany(SafeGetTypes) |
| 2202 | + .Where(IsValidComponent) |
| 2203 | + .Where(match); |
| 2204 | + var fromEditor = editorAsms.SelectMany(SafeGetTypes) |
| 2205 | + .Where(IsValidComponent) |
| 2206 | + .Where(match); |
| 2207 | + |
| 2208 | + var list = new List<Type>(fromPlayer); |
| 2209 | + if (list.Count == 0) list.AddRange(fromEditor); |
| 2210 | + return list; |
| 2211 | + } |
2093 | 2212 |
|
2094 | | - // If not found, search all loaded assemblies (slower, last resort) |
2095 | | - // Prioritize assemblies likely to contain game/editor types |
2096 | | - Assembly[] priorityAssemblies = { |
2097 | | - Assembly.Load("Assembly-CSharp"), // Main game scripts |
2098 | | - Assembly.Load("Assembly-CSharp-Editor"), // Main editor scripts |
2099 | | - // Add other important project assemblies if known |
2100 | | - }; |
2101 | | - foreach (var assembly in priorityAssemblies.Where(a => a != null)) |
2102 | | - { |
2103 | | - type = assembly.GetType(typeName) ?? assembly.GetType("UnityEngine." + typeName) ?? assembly.GetType("UnityEditor." + typeName); |
2104 | | - if (type != null) return type; |
2105 | | - } |
| 2213 | +#if UNITY_EDITOR |
| 2214 | + private static IEnumerable<Type> PreferPlayer(IEnumerable<Type> seq) |
| 2215 | + { |
| 2216 | + var player = new HashSet<string>( |
| 2217 | + UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name), |
| 2218 | + StringComparer.Ordinal); |
2106 | 2219 |
|
2107 | | - // Search remaining assemblies |
2108 | | - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Except(priorityAssemblies)) |
2109 | | - { |
2110 | | - try { // Protect against assembly loading errors |
2111 | | - type = assembly.GetType(typeName); |
2112 | | - if (type != null) return type; |
2113 | | - // Also check with common namespaces if simple name given |
2114 | | - foreach (string ns in namespaces) { |
2115 | | - type = assembly.GetType($"{ns}.{typeName}"); |
2116 | | - if (type != null) return type; |
2117 | | - } |
2118 | | - } catch (Exception ex) { |
2119 | | - Debug.LogWarning($"[FindType] Error searching assembly {assembly.FullName}: {ex.Message}"); |
2120 | | - } |
2121 | | - } |
| 2220 | + return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1); |
| 2221 | + } |
| 2222 | +#endif |
2122 | 2223 |
|
2123 | | - Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'"); |
2124 | | - return null; // Not found |
| 2224 | + private static string Ambiguity(string query, IEnumerable<Type> cands) |
| 2225 | + { |
| 2226 | + var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})"); |
| 2227 | + return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) + |
| 2228 | + "\nProvide a fully qualified type name to disambiguate."; |
2125 | 2229 | } |
2126 | 2230 |
|
2127 | 2231 | /// <summary> |
|
0 commit comments