using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Unity.Pipeline.HotReload; using UnityEngine; #if UNITY_6000_3_OR_NEWER using UnityEngine.Assemblies; #endif namespace Unity.Pipeline.Commands { /// /// Registry for discovering and managing CLI commands via pluggable discovery mechanism. /// Scans for methods marked with [CliCommand] attribute across all assemblies. /// Based on unity-tools ToolRegistry patterns adapted for Pipeline requirements. /// public static class CommandRegistry { private static IReadOnlyList m_CachedCommands; private static ICommandDiscovery m_Discovery; /// /// Set the command discovery mechanism. /// Editor assembly provides TypeCache-based discovery, Runtime uses reflection fallback. /// public static void SetDiscovery(ICommandDiscovery discovery) { m_Discovery = discovery; ClearCache(); // Re-discover with new mechanism } /// /// Discover all CLI commands marked with [CliCommand] attribute. /// Uses injected discovery mechanism (TypeCache in Editor, reflection in Runtime). /// Results are cached until domain reload. /// public static IEnumerable DiscoverCommands() { if (m_CachedCommands == null) { m_CachedCommands = DiscoverCommandsInternal().ToList(); // Also discover hot reload methods when commands are discovered DiscoverHotReloadMethods(); } return m_CachedCommands; } /// /// Clear the command cache. Called automatically on domain reload. /// Can be called manually for testing or dynamic command registration. /// public static void ClearCache() { m_CachedCommands = null; } /// /// Internal implementation of command discovery using injected discovery mechanism. /// private static IEnumerable DiscoverCommandsInternal() { var commands = new List(); try { // Use injected discovery mechanism if available, otherwise fallback to reflection IEnumerable methods; if (m_Discovery != null) { methods = m_Discovery.GetMethodsWithAttribute(); } else { // Fallback: Use reflection to scan loaded assemblies methods = GetMethodsWithAttributeViaReflection(); } foreach (var method in methods) { try { var commandInfo = CreateCommandInfo(method); if (commandInfo != null) { commands.Add(commandInfo); } } catch (Exception ex) { Debug.LogWarning($"Failed to register command from method {method.DeclaringType?.Name}.{method.Name}: {ex.Message}"); } } } catch (Exception ex) { Debug.LogError($"Failed to discover commands: {ex.Message}"); } return commands; } /// /// Fallback command discovery using reflection when TypeCache is not available. /// private static IEnumerable GetMethodsWithAttributeViaReflection() where T : Attribute { var methods = new List(); try { var assemblies = PipelineUtils.GetLoadedAssemblies(); foreach (var assembly in assemblies) { // Skip system assemblies for performance if (IsSystemAssembly(assembly)) continue; try { foreach (var type in assembly.GetTypes()) { foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { if (method.GetCustomAttribute() != null) { methods.Add(method); } } } } catch (ReflectionTypeLoadException) { // Skip assemblies that can't be loaded continue; } } } catch (Exception ex) { Debug.LogWarning($"Reflection-based command discovery failed: {ex.Message}"); } return methods; } /// /// Check if assembly is a system assembly that should be skipped during discovery. /// private static bool IsSystemAssembly(Assembly assembly) { var name = assembly.GetName().Name; return name.StartsWith("System.") || name.StartsWith("Microsoft.") || name.StartsWith("mscorlib") || name.StartsWith("netstandard") || name.Equals("UnityEngine") || name.Equals("UnityEditor"); } /// /// Create CommandInfo from a method with CliCommand attribute. /// private static CommandInfo CreateCommandInfo(MethodInfo method) { var commandAttr = method.GetCustomAttribute(); if (commandAttr == null) return null; // Validate method is static (required for CLI commands). // Any static method may be registered regardless of accessibility // (public, internal, or private) — invocation goes through MethodInfo.Invoke. if (!method.IsStatic) { Debug.LogWarning($"Command method {method.DeclaringType?.Name}.{method.Name} must be static"); return null; } // Discover parameters var parameters = DiscoverParameters(method).ToList(); return new CommandInfo( commandAttr.Name, commandAttr.Description, commandAttr.MainThreadRequired, method, parameters, commandAttr.RuntimeOnly ); } /// /// Discover parameter information from method parameters. /// private static IEnumerable DiscoverParameters(MethodInfo method) { foreach (var param in method.GetParameters()) { var argAttr = param.GetCustomAttribute(); // Parameters without CliArg attribute get default metadata var name = argAttr?.Name ?? param.Name; var description = argAttr?.Description ?? $"Parameter: {param.Name}"; var required = argAttr?.Required ?? !param.HasDefaultValue; var defaultValue = param.HasDefaultValue ? param.DefaultValue : argAttr?.DefaultValue; yield return new CommandParameterInfo(name, description, required, param.ParameterType, defaultValue); } } /// /// Discover and register methods marked with [HotReloadWithOverrides] attribute. /// Called during command discovery to populate the hot reload registry. /// private static void DiscoverHotReloadMethods() { try { // Use injected discovery mechanism if available, otherwise fallback to reflection IEnumerable methods; if (m_Discovery != null) { methods = m_Discovery.GetMethodsWithAttribute(); } else { // Fallback: Use reflection to scan loaded assemblies methods = GetMethodsWithAttributeViaReflection(); } foreach (var method in methods) { try { var hotReloadAttr = method.GetCustomAttribute(); if (hotReloadAttr != null) { HotReloadRegistry.RegisterReloadableMethod(method, hotReloadAttr); } } catch (Exception ex) { Debug.LogWarning($"Failed to register hot reload method {method.DeclaringType?.Name}.{method.Name}: {ex.Message}"); } } } catch (Exception ex) { Debug.LogError($"Failed to discover hot reload methods: {ex.Message}"); } } } }