feat: implement robust ComponentResolver for assembly definitions

- Replace Assembly.LoadFrom with already-loaded assembly search
- Prioritize Player assemblies over Editor assemblies using CompilationPipeline
- Support both short names and fully-qualified component names
- Add comprehensive caching and error handling with disambiguation
- Use Undo.AddComponent for proper editor integration
- Handle ReflectionTypeLoadException safely during type enumeration
- Add fallback to TypeCache for comprehensive type discovery in Editor

Fixes component resolution across custom assembly definitions and eliminates
"Could not load the file 'Assembly-CSharp-Editor'" errors.

Tested with:
- Built-in Unity components (Rigidbody, MeshRenderer, AudioSource)
- Custom user scripts in Assembly-CSharp (TicTacToe3D)
- Custom assembly definition components (TestNamespace.CustomComponent)
- Error handling for non-existent components

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
main
David Sarno 2025-09-02 18:45:30 -07:00
parent 43d1d91327
commit c40f3d0357
6 changed files with 193 additions and 88 deletions

View File

@ -0,0 +1,18 @@
using UnityEngine;
namespace TestNamespace
{
public class CustomComponent : MonoBehaviour
{
[SerializeField]
private string customText = "Hello from custom asmdef!";
[SerializeField]
private float customFloat = 42.0f;
void Start()
{
Debug.Log($"CustomComponent started: {customText}, value: {customFloat}");
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 78ee39b9744834fe390a4c7c5634eb5a

View File

@ -0,0 +1,14 @@
{
"name": "TestAsmdef",
"rootNamespace": "TestNamespace",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 72f6376fa7bdc4220b11ccce20108cdc
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -201,7 +201,7 @@ namespace MCPForUnity.Editor.Tools
"'scriptClass' property required when creating ScriptableObject asset."
);
Type scriptType = FindType(scriptClassName);
Type scriptType = ComponentResolver.TryResolve(scriptClassName, out var resolvedType, out var error) ? resolvedType : null;
if (
scriptType == null
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType)
@ -355,7 +355,7 @@ namespace MCPForUnity.Editor.Tools
{
// Find the component on the GameObject using the name from the JSON key
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous.
// Consider using FindType helper if needed for more complex scenarios.
// Consider using ComponentResolver if needed for more complex scenarios.
Component targetComponent = gameObject.GetComponent(componentName);
if (targetComponent != null)
@ -1220,46 +1220,6 @@ namespace MCPForUnity.Editor.Tools
}
}
/// <summary>
/// Helper to find a Type by name, searching relevant assemblies.
/// Needed for creating ScriptableObjects or finding component types by name.
/// </summary>
private static Type FindType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
return null;
// Try direct lookup first (common Unity types often don't need assembly qualified name)
var type =
Type.GetType(typeName)
?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule")
?? Type.GetType($"UnityEngine.UI.{typeName}, UnityEngine.UI")
?? Type.GetType($"UnityEditor.{typeName}, UnityEditor.CoreModule");
if (type != null)
return type;
// If not found, search loaded assemblies (slower but more robust for user scripts)
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// Look for non-namespaced first
type = assembly.GetType(typeName, false, true); // throwOnError=false, ignoreCase=true
if (type != null)
return type;
// Check common namespaces if simple name given
type = assembly.GetType("UnityEngine." + typeName, false, true);
if (type != null)
return type;
type = assembly.GetType("UnityEditor." + typeName, false, true);
if (type != null)
return type;
// Add other likely namespaces if needed (e.g., specific plugins)
}
Debug.LogWarning($"[FindType] Type '{typeName}' not found in any loaded assembly.");
return null; // Not found
}
// --- Data Serialization ---

View File

@ -1,3 +1,4 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@ -5,6 +6,7 @@ using System.Reflection;
using Newtonsoft.Json; // Added for JsonSerializationException
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Compilation; // For CompilationPipeline
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;
@ -1097,6 +1099,29 @@ namespace MCPForUnity.Editor.Tools
// --- Internal Helpers ---
/// <summary>
/// Parses a JArray like [x, y, z] into a Vector3.
/// </summary>
private static Vector3? ParseVector3(JArray array)
{
if (array != null && array.Count == 3)
{
try
{
return new Vector3(
array[0].ToObject<float>(),
array[1].ToObject<float>(),
array[2].ToObject<float>()
);
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to parse JArray as Vector3: {array}. Error: {ex.Message}");
}
}
return null;
}
/// <summary>
/// Finds a single GameObject based on token (ID, name, path) and search method.
/// </summary>
@ -2070,58 +2095,137 @@ namespace MCPForUnity.Editor.Tools
/// <summary>
/// Helper to find a Type by name, searching relevant assemblies.
/// Robust component resolver that avoids Assembly.LoadFrom and works with asmdefs.
/// Searches already-loaded assemblies, prioritizing runtime script assemblies.
/// </summary>
private static Type FindType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error))
{
return resolvedType;
}
// Log the resolver error if type wasn't found
if (!string.IsNullOrEmpty(error))
{
Debug.LogWarning($"[FindType] {error}");
}
return null;
// Handle fully qualified names first
Type type = Type.GetType(typeName);
if (type != null) return type;
// Handle common namespaces implicitly (add more as needed)
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
foreach (string ns in namespaces) {
type = Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}.CoreModule") ?? // Heuristic: Check CoreModule first for UnityEngine/UnityEditor
Type.GetType($"{ns}.{typeName}, {ns.Split('.')[0]}"); // Try assembly matching namespace root
if (type != null) return type;
}
}
// If not found, search all loaded assemblies (slower, last resort)
// Prioritize assemblies likely to contain game/editor types
Assembly[] priorityAssemblies = {
Assembly.Load("Assembly-CSharp"), // Main game scripts
Assembly.Load("Assembly-CSharp-Editor"), // Main editor scripts
// Add other important project assemblies if known
};
foreach (var assembly in priorityAssemblies.Where(a => a != null))
/// <summary>
/// Robust component resolver that avoids Assembly.LoadFrom and supports assembly definitions.
/// Prioritizes runtime (Player) assemblies over Editor assemblies.
/// </summary>
internal static class ComponentResolver
{
type = assembly.GetType(typeName) ?? assembly.GetType("UnityEngine." + typeName) ?? assembly.GetType("UnityEditor." + typeName);
if (type != null) return type;
}
private static readonly Dictionary<string, Type> CacheByFqn = new(StringComparer.Ordinal);
private static readonly Dictionary<string, Type> CacheByName = new(StringComparer.Ordinal);
// Search remaining assemblies
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Except(priorityAssemblies))
/// <summary>
/// Resolve a Component/MonoBehaviour type by short or fully-qualified name.
/// Prefers runtime (Player) script assemblies; falls back to Editor assemblies.
/// Never uses Assembly.LoadFrom.
/// </summary>
public static bool TryResolve(string nameOrFullName, out Type type, out string error)
{
try { // Protect against assembly loading errors
type = assembly.GetType(typeName);
if (type != null) return type;
// Also check with common namespaces if simple name given
foreach (string ns in namespaces) {
type = assembly.GetType($"{ns}.{typeName}");
if (type != null) return type;
}
} catch (Exception ex) {
Debug.LogWarning($"[FindType] Error searching assembly {assembly.FullName}: {ex.Message}");
}
error = string.Empty;
// 1) Exact FQN via Type.GetType
if (CacheByFqn.TryGetValue(nameOrFullName, out type)) return true;
type = Type.GetType(nameOrFullName, throwOnError: false);
if (IsValidComponent(type)) { Cache(type); return true; }
// 2) Search loaded assemblies (prefer Player assemblies)
var candidates = FindCandidates(nameOrFullName);
if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
#if UNITY_EDITOR
// 3) Last resort: Editor-only TypeCache (fast index)
var tc = TypeCache.GetTypesDerivedFrom<Component>()
.Where(t => NamesMatch(t, nameOrFullName));
candidates = PreferPlayer(tc).ToList();
if (candidates.Count == 1) { type = candidates[0]; Cache(type); return true; }
if (candidates.Count > 1) { error = Ambiguity(nameOrFullName, candidates); type = null!; return false; }
#endif
error = $"Component type '{nameOrFullName}' not found in loaded runtime assemblies. " +
"Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled.";
type = null!;
return false;
}
Debug.LogWarning($"[FindType] Type not found after extensive search: '{typeName}'");
return null; // Not found
private static bool NamesMatch(Type t, string q) =>
t.Name.Equals(q, StringComparison.Ordinal) ||
(t.FullName?.Equals(q, StringComparison.Ordinal) ?? false);
private static bool IsValidComponent(Type? t) =>
t != null && typeof(Component).IsAssignableFrom(t);
private static void Cache(Type t)
{
if (t.FullName != null) CacheByFqn[t.FullName] = t;
CacheByName[t.Name] = t;
}
private static List<Type> FindCandidates(string query)
{
bool isShort = !query.Contains('.');
var loaded = AppDomain.CurrentDomain.GetAssemblies();
#if UNITY_EDITOR
// Names of Player (runtime) script assemblies (asmdefs + Assembly-CSharp)
var playerAsmNames = new HashSet<string>(
UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
StringComparer.Ordinal);
IEnumerable<System.Reflection.Assembly> playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name));
IEnumerable<System.Reflection.Assembly> editorAsms = loaded.Except(playerAsms);
#else
IEnumerable<System.Reflection.Assembly> playerAsms = loaded;
IEnumerable<System.Reflection.Assembly> editorAsms = Array.Empty<System.Reflection.Assembly>();
#endif
static IEnumerable<Type> SafeGetTypes(System.Reflection.Assembly a)
{
try { return a.GetTypes(); }
catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null)!; }
}
Func<Type, bool> match = isShort
? (t => t.Name.Equals(query, StringComparison.Ordinal))
: (t => t.FullName!.Equals(query, StringComparison.Ordinal));
var fromPlayer = playerAsms.SelectMany(SafeGetTypes)
.Where(IsValidComponent)
.Where(match);
var fromEditor = editorAsms.SelectMany(SafeGetTypes)
.Where(IsValidComponent)
.Where(match);
var list = new List<Type>(fromPlayer);
if (list.Count == 0) list.AddRange(fromEditor);
return list;
}
#if UNITY_EDITOR
private static IEnumerable<Type> PreferPlayer(IEnumerable<Type> seq)
{
var player = new HashSet<string>(
UnityEditor.Compilation.CompilationPipeline.GetAssemblies(UnityEditor.Compilation.AssembliesType.Player).Select(a => a.name),
StringComparer.Ordinal);
return seq.OrderBy(t => player.Contains(t.Assembly.GetName().Name) ? 0 : 1);
}
#endif
private static string Ambiguity(string query, IEnumerable<Type> cands)
{
var lines = cands.Select(t => $"{t.FullName} (assembly {t.Assembly.GetName().Name})");
return $"Multiple component types matched '{query}':\n - " + string.Join("\n - ", lines) +
"\nProvide a fully qualified type name to disambiguate.";
}
/// <summary>